Java 干货之深刻理解Java内部类

能够将一个类定义在另外一个类或方法中,这样的类叫作内部类 --《Thinking in Java》java

提及内部类,你们并不陌生,而且会常常在实例化容器的时候使用到它。可是内部类的具体细节语法,原理以及实现是什么样的能够很多人都还挺陌生,这里做一篇总结,但愿经过这篇总结提升对内部类的认识。程序员


内部类是什么?

由文章开头可知,内部类的定义为:定义在另外一个类或方法中的类。而根据使用场景的不一样,内部类还能够分为四种:成员内部类,局部内部类,匿名内部类和静态内部类。每一种的特性和注意事项都不一样,下面咱们一一说明。编程

成员内部类

顾名思义,成员内部类是定义在类内部,做为类的成员的类。以下:bash

public class Outer {
    
   public class Inner{
       
   }

}

复制代码

特色以下:微信

  1. 成员内部类能够被权限修饰符(eg. public,private等)所修饰
  2. 成员内部类能够访问外部类的全部成员,(包括private)成员
  3. 成员内部类是默认包含了一个指向外部类对象的引用
  4. 如同使用this同样,当成员名或方法名发生覆盖时,可使用外部类的名字加.this指定访问外部类成员。如:Outer.this.name
  5. 成员内部类不能够定义static成员
  6. 成员内部类建立语法:
Outer outer=new Outer();
Outer.Inner inner=outer.new Inner();
复制代码

局部内部类

局部内部类是定义在方法或者做用域中类,它和成员内部类的区别仅在于访问权限的不一样。闭包

public class Outer{
    public void test(){
        class Inner{
            
        }
    }
}
复制代码

特色以下:编程语言

  1. 局部内部类不能有访问权限修饰符ide

  2. 局部内部类不能被定义为static函数

  3. 局部内部类不能定义static成员ui

  4. 局部内部类默认包含了外部类对象的引用

  5. 局部内部类也可使用Outer.this语法制定访问外部类成员

  6. 局部内部类想要使用方法或域中的变量,该变量必须是final

    在JDK1.8 之后,没有final修饰,effectively final的便可。什么意思呢?就是没有final修饰,可是若是加上final编译器也不会报错便可。

匿名内部类

匿名内部类是与继承合并在一块儿的没有名字的内部类

public class Outer{
    public List<String> list=new ArrayList<String>(){
        {
            add("test");
        }
    };
}
复制代码

这是咱们平时最经常使用的语法。 匿名内部类的特色以下:

  1. 匿名内部类使用单独的块表示初始化块{}
  2. 匿名内部类想要使用方法或域中的变量,该变量必须是final修饰的,JDK1.8以后effectively final也能够
  3. 匿名内部类默认包含了外部类对象的引用
  4. 匿名内部类表示继承所依赖的类

嵌套类

嵌套类是用static修饰的成员内部类

public class Outer {
    
   public static class Inner{
       
   }

}
复制代码

特色以下:

  1. 嵌套类是四种类中惟一一个不包含对外部类对象的引用的内部类

  2. 嵌套类能够定义static成员

  3. 嵌套类能访问外部类任何静态数据成员与方法。

    构造函数能够看做静态方法,所以能够访问。


为何要有内部类?

从上面能够看出,内部类的特性和类方差很少,可是内部类有许多繁琐的细节语法。既然内部类有这么多的细节要注意,那为何Java还要支持内部类呢?

1. 完善多重继承
  1. 在早期C++做为面向对象编程语言的时候,最难处理的也就是多重继承,多重继承对于代码耦合度,代码使用人员的理解来讲,并不怎么友好,而且还要比较出名的死亡菱形的多重继承问题。所以Java并不支持多继承。
  2. 后来,Java设计者发现,没有多继承,一些代码友好的设计与编程问题变得十分难以解决。因而便产生了内部类。内部类具备:隐式包含外部类对象而且可以与之通讯的特色,完美的解决了多重继承的问题。
2. 解决屡次实现/继承问题
  1. 有时候在一个类中,须要屡次经过不一样的方式实现同一个接口,若是没有内部类,必须屡次定义不一样数量的类,可是使用内部类能够很好的解决这个问题,每一个内部类均可以实现同一个接口,即实现了代码的封装,又实现了同一接口不一样的实现。

  2. 内部类能够将组合的实现封装在内部中。


为何内部类的语法这么繁杂

这一点是本文的重点。内部类语法之因此这么繁杂,是由于它是新数据类型加语法糖的结合。想要理解内部类,还得从本质上出发.

内部类根据应用场景的不一样分为4种。其应用场景彻底能够和类方法对比起来。
下面咱们经过类方法对比的模式一一解答为何内部类会有这样的特色

成员内部类——>成员方法

成员内部类的设计彻底和成员方法同样。
调用成员方法:outer.getName()
新建内部类对象:outer.new Inner()
它们都是要依赖对象而被调用。 正如《Thinking in Java》所说,outer.getName()正真的形似是Outer.getName(outer),也就是将调用对象做为参数传递给方法。
新建一个内部类也是这样:Outer.new Inner(outer)

下面,咱们用实际状况证实: 新建一个包含内部类的类:

public class Outer {

    private int m = 1;

    public class Inner {
    
        private void test() {
            //访问外部类private成员
            System.out.println(m);
        }
    }
}
复制代码

编译,会发现会在编译目标目录生成两个.class文件:Outer.classOuter$Inner.class

PS:不知道为何Java老是和过不去,就连变量命名规则都要比C++多一个能由组成 :)

Outer$Inner.class放入IDEA中打开,会自动反编译,查看结果:

public class Outer$Inner {
    public Outer$Inner(Outer this$0) {
        this.this$0 = this$0;
    }

    private void test() {
        System.out.println(Outer.access$000(this.this$0));
    }
}
复制代码

能够看见,编译器已经自动生成了一个默认构造器,这个默认构造器是一个带有外部类型引用的参数构造器。

能够看到外部类成员对象的引用:Outer是由final修饰的。

所以:

  1. 成员内部类做为类级成员,所以能被访问修饰符所修饰
  2. 成员内部类中包含建立内部类时对外部类对象的引用,因此成员内部类能访问外部类的全部成员。
  3. 语法规定:由于它做为外部类的一部分红员,因此即便private的对象,内部类也能访问。。经过Outer.access$ 指令访问
  4. 如同非静态方法不能访问静态成员同样,非静态内部类也被设计的不能拥有静态变量,所以内部类不能定义static对象和方法。

可是能够定义static final变量,这并不冲突,由于所定义的final字段必须是编译时肯定的,并且在编译类时会将对应的变量替换为具体的值,因此在JVM看来,并无访问内部类。

局部内部类——> 局部代码块

局部内部类能够和局部代码块相理解。它最大的特色就是只能访问外部的final变量。 先别着急问为何。
定义一个局部内部类:

public class Outer {

    private void test() {

        int  m= 3;
        class Inner {
            private void print() {
                System.out.println(m);
            }
        }
    }

}
复制代码

编译,发现生成两个.class文件Outer.classOuter$1Inner.classOuter$1Inner.class放入IDEA中反编译:

class Outer$1Inner {
    Outer$1Inner(Outer this$0, int var2) {
        this.this$0 = this$0;
        this.val$m = var2;
    }

    private void print() {
        System.out.println(this.val$m);
    }
}

复制代码

能够看见,编译器自动生成了带有两个参数的默认构造器。
看到这里,也许应该能明了:咱们将代码转换下:

public class Outer {
    private void test() {
        int  m= 3;
        Inner inner=new Outer$1Inner(this,m);
        
        inner.print();
        }
    }

}
复制代码

也就是在Inner中,实际上是将m的值,拷贝到内部类中的。print()方法只是输出了m,若是咱们写出了这样的代码:

private void test() {

        int  m= 3;

        class Inner {

            private void print() {
               m=4;
            }
        }
        
       System.out.println(m);  
    }
复制代码

在咱们看来,m的值应该被修改成4,可是它真正的效果是:

private void test(){
    int m = 3;
    
    print(m);
    
    System.out.println(m);
}

private void print(int m){
    m=4;
}
复制代码

m被做为参数拷贝进了方法中。所以修改它的值其实没有任何效果,因此为了避免让程序员随意修改m而却没达到任何效果而迷惑,m必须被final修饰。

绕了这么大一圈,为何编译器要生成这样的效果呢?
其实,了解闭包的概念的人应该都知道缘由。而Java中各类诡异的语法通常都是由生命周期带来的影响。上面的程序中,m是一个局部变量,它被定义在栈上,而new Outer$1Inner(this,m);所生成的对象,是定义在堆上的。若是不将m做为成员变量拷贝进对象中,那么离开m的做用域,Inner对象所指向的即是一个无效的地址。所以,编译器会自动将局部类所使用的全部参数自动生成成员。

为何其余语言没有这种现象呢?
这又回到了一个经典的问题上:Java是值传递仍是引用传递。因为Java always pass-by-value,对于真正的引用,Java是没法传递过去的。而上面的问题核心就在与m若是被改变了,那么其它的m的副本是没法感知到的。而其余语言都经过其余的途径解决了这个问题。
对于C++就是一个指针问题

理解了真正的缘由,便也能知道何时须要final,何时不须要final了。

public class Outer {
    private void test() {
        class Inner {
        int m=3;
            private void print() {
                System.out.println(m);//做为参数传递,自己都已经 pass-by-value。不用final
                int c=m+1; //直接使用m,须要加final
                
            }
        }
    }

}
复制代码

而在Java 8 中,已经放宽政策,容许是effectively final的变量,实际上,就是编译器在编译的过程当中,帮你加上final而已。而你应该保证容许编译器加上final后,程序不报错。

  1. 局部内部类还有个特色就是不能有权限修饰符。就好像局部变量不能有访问修饰符同样

  2. 由上面能够看到,外部对象一样是被传入局部类中,所以局部类能够访问外部对象

嵌套类——>静态方法

嵌套类没什么好说的,就好像静态方法同样,他能够被直接访问,他也能定义静态变量。同时不能访问非静态成员。
值得注意的是《Think in Java》中说过,能够将构造函数看做为静态方法,所以嵌套类能够访问外部类的构造方法。

匿名类——>局部方法+继承的语法糖

匿名类能够看做是对前3种类的再次扩展。具体来讲匿名类根据应用场景能够看做:

  • 成员内部类+继承
  • 局部内部类+继承
  • 嵌套内部类+继承

匿名类语法为:

new 继承类名(){
  
  //Override 重载的方法    
    
}
复制代码

返回的结果会向上转型为继承类。

声明一个匿名类:

public class Outer {

    private  List<String> list=new ArrayList<String>(){
        {
            add("test");
        }
    };

}
复制代码

这即是一个经典的匿名类用法。 一样编译上面代码会看到生成了两个.class文件Outer.class,Outer$1.classOuter$1.class放入IDEA中反编译:

class Outer$1 extends ArrayList<String> {
    Outer$1(Outer this$0) {
        this.this$0 = this$0;
        this.add("1");
    }
}

复制代码

能够看到匿名类的完整语法即是继承+内部类。
因为匿名类能够申明为成员变量,局部变量,静态成员变量,所以它的组合即是几种内部类加继承的语法糖,这里不一一证实。
在这里值得注意的是匿名类因为没有类名,所以不能经过语法糖像正常的类同样声明构造函数,可是编译器能够识别{},并在编译的时候将代码放入构造函数中。

{}能够有多个,会在生成的构造函数中按顺序执行。


怎么正确的使用内部类

在第二小节中,咱们已经讨论过内部类的应用场景,可是如何优雅,并在正确的应用场景使用它呢?本小节将会详细讨论。

1.注意内存泄露

《Effective Java》第二十四小节明确提出过。优先使用静态内部类。这是为何呢? 由上面的分析咱们能够知道,除了嵌套类,其余的内部类都隐式包含了外部类对象。这即是Java内存泄露的源头。看代码:

定义Outer:

public class Outer{

    public  List<String> getList(String item) {

        return new ArrayList<String>() {
            {
                add(item);
            }
        };
    }
}
复制代码

使用Outer:

public class Test{

   public static List<String> getOutersList(){
   
    Outer outer=new Outer();
    //do something
    List<String> list=outer.getList("test");
   
    return list;    
   }
   public static void main(String[] args){
       List<String> list=getOutersList();
       
      
      //do something with list
   }
   
}

复制代码

相信这样的代码必定有同窗写出来,这涉及到一个习惯的问题:

不涉及到类成员方法和成员变量的方法,最好定义为static

咱们先研究上面的代码,最大的问题即是带来的内存泄露:
在使用过程当中,咱们定义Outer对象完成一系列的动做

  • 使用outer获得了一个ArraList对象
  • ArrayList做为结果返回出去。

正常来讲,在getOutersList方法中,咱们new出来了两个对象:outerlist,而在离开此方法时,咱们只将list对象的引用传递出去,outer的引用随着方法栈的退出而被销毁。按道理来讲,outer对象此时应该没有做用了,也应该在下一次内存回收中被销毁。

然而,事实并非这样。按上面所说的,新建的list对象是默认包含对outer对象的引用的,所以只要list不被销毁,outer对象将会一直存在,然而咱们并不须要outer对象,这即是内存泄露。

怎么避免这种状况呢?

很简单:不涉及到类成员方法和成员变量的方法,最好定义为static

public class Outer{

    public static List<String> getList(String item) {

        return new ArrayList<String>() {
            {
                add(item);
            }
        };
    }
}
复制代码

这样定义出来的类即是嵌套类+继承,并不包含对外部类的引用。

2.应用于只实现一个接口的实现类

  • 优雅工厂方法模式

咱们能够看到,在工厂方法模式中,每一个实现都会须要实现一个Fractory来实现产生对象的接口,而这样接口其实和本来的类关联性很大的,所以咱们能够将Fractory定义在具体的类中,做为内部类存在

  • 简单的实现接口
new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("test");
           }
       }

       ).start();
    }
复制代码

尽可能不要直接使用Thread,这里只作演示使用 Java 8 的话建议使用lambda代替此类应用

  • 同时实现多个接口
public class imple{

    public static Eat getDogEat(){
        return new EatDog();
    }

    public static Eat getCatEat(){
        return new EatCat();
    }

    private static class EatDog implements Eat {
        @Override
        public void eat() {
            System.out.println("dog eat");
        }
    }
    private static class EatCat implements Eat{
        @Override
        public void eat() {
            System.out.println("cat eat");
        }
    }
}
复制代码

3.优雅的单例类

public class Imple {

    public static Imple getInstance(){
        return ImpleHolder.INSTANCE;
    }


    private static class ImpleHolder{
        private static final Imple INSTANCE=new Imple();
    }
}
复制代码

4.反序列化JSON接受的JavaBean 有时候须要反序列化嵌套JSON

{
    "student":{
        "name":"",
        "age":""
    }
}
复制代码

相似这种。咱们能够直接定义嵌套类进行反序列化

public JsonStr{
    
    private Student student;
    
    public static Student{
        private String name;
        private String age;
        
        //getter & setter
    }

    //getter & setter
}

复制代码

可是注意,这里应该使用嵌套类,由于咱们不须要和外部类进行数据交换。

核心思想:

  • 嵌套类可以访问外部类的构造函数
  • 将第一次访问内部类放在方法中,这样只有调用这个方法的时候才会第一次访问内部类,实现了懒加载

内部类还有不少用法,这里不一一列举。


总结

内部类的理解能够按照方法来理解,可是内部类不少特性都必须剥开语法糖和明白为何须要这么作才能彻底理解,明白内部类的全部特性才能更好使用内部类,在内部类的使用过程当中,必定记住:能使用嵌套类就使用嵌套类,若是内部类须要和外部类联系,才使用内部类。最后不涉及到类成员方法和成员变量的方法,最好定义为static能够防止内部类内存泄露。

尊重劳动成果,转载请标注出处。


若是以为写得不错,欢迎关注微信公众号:逸游Java ,天天不定时发布一些有关Java干货的文章,感谢关注

参考文章:
Java 中引入内部类的意义?
成员内部类里面为何不能有静态成员和方法?

相关文章
相关标签/搜索