9.内部类

9.1.相关概念
  • 将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外围类。在它们外边的无关的类称为外部类。
  • 成员内部类:定义在类中方法外的类。在外围类的方法中可以使用。
  • 局部内部类:定义在成员方法中的类。只能在成员方法中使用,出了成员方法的范围则不可使用。
  • 静态内部类:也是定义在外围类成员位置的内部类,可以理解为是一个特殊的成员内部类。
  • 匿名内部类:匿名内部类是特殊的局部内部类,必须定义在成员位置。

    注:

    ​ 内部类可以直接访问外围类的成员,包括私有成员。

    ​ 外围类要访问内部类的成员,必须要建立内部类的对象。

9.2.成员内部类
9.2.1.权限修饰符
  • 可以是public,protected,(default)缺省,private
9.2.2.成员特点
  • 成员变量、成员方法、构造器。

  • 可以定义普通成员变量,成员方法

    • 不能定义静态方法、静态成员、静态代码块
    • 可以定义全局常量(因为全局常量是编译时就加入了常量池)
  • 可以定义构造方法,和普通类并无差别

9.2.3.成员内部类的类加载机制
  • 一个外围类中定义了成员内部类后,并不代表每个外围类对象中都会自动初始化一个内部类对象。
  • 内部类都是延时加载的,也就是说只有在第一次使用的时候才会被加载
  • 只有在外围类对象的基础上,创建内部类的对象,才会触发成员内部类的类加载
  • 也就是说,成员内部类对象依赖于外围类的对象(即成员内部类实例化是在外围类实例的基础上)
    • 任何时候都是这样,即便是反射,也没办法不创建外围类对象而创建成员内部类对象
9.2.4.继承和实现
  • 内部类可以继承和实现外部的类和接口
  • 甚至可以在类中定义多个普通类、抽象内部类和接口用来自己继承和实现
9.2.5.成员内部类的访问特点
  • 成员内部类内部访问外围类
    • 需要理解的是,成员内部类相当于外围类的成员,并且内部类对象依赖外围类,在外围类内部访问时,一定存在一个外围类对象(重要)
      • 成员内部类可以无条件访问外围类的所有成员属性和成员方法(包括private成员和静态成员)需要注意的是,当外围类中有同名的属性或者方法时,都会发生类似“隐藏”的现象,即默认情况下访问的都是成员内部类的成员
    • 如果要访问外围类的同名成员,下面为例子
    外围类名.this.成员变量
    外围类名.this.成员方法
    
    • 这里有一个类似this、super一样的隐式引用,默认传给内部类的所有成员方法
  • 外围类访问成员内部类成员
    • 虽然成员内部类可以无条件地访问外围类的成员,而外围类想访问成员内部类的成员却不是这么随心所欲了,原因在于内部类访问外围类时,内部类对象和外围类对象一定都已经存在,但外围类访问内部类时,内部类对象还不存在,所以必须手动创建内部类对象,当然如果想要访问内部类中的全局常量,直接内部类名点常量名即可。
    • 在外围类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过这个内部类的引用去访问内部类的成员
    • 在外围类中创建内部类语法:最容易理解,最万能的形式如下:
    //该形式适合在外围类中创建内部类
    外围类名 对象名 = new 外围类构造函数();
    内部类名 对象名 = 外围类对象名.new 内部类构造函数();
    //或者
    外部类名.内部类名 对象名 = new 外围类构造函数().new 内部类构造函数();
    

    如果访问内部类的方法是静态的,则只能使用上述格式,因为静态方法中外围类对象可能不存在,如果访问内部类的方法是普通成员方法,则可以省略创建外围类对象的过程。在外围类中一旦创建内部类对象,使用该对象可以访问内部类的所有成员,包括私有

  • 外部类访问成员内部类成员
    • 外部类要访问内部类成员,条件要苛刻的多,由于内部类属于外围类的一个成员,所以内部类受访问权限的限制。
    • 如果该内部类是private修饰,那么显然在任何外部类中都无法访问该内部类成员。
    • 如果外部类拥有内部类的访问权限,可以创建该内部类对象,来访问该内部类的成员。
    • 和外围类创建内部类对象不同,在外部类中创建内部类对象,不能够访问内部类的私有成员
  • 成员内部类访问外部类成员
    • 在成员内部类中访问外部类成员,和在普通类中访问其它类成员别无二致
    • 静态成员直接 类名.成员名 访问
    • 普通成员需创建外部类对象
    • 受访问权限控制
9.2.6.成员内部类、外围类、外部类互相访问总结
  1. 内部类的成员方法中,去访问外围类的成员(普通或静态、私有成员)
    • 外围类对象的引用:外围类类名.this.变量名

在这里有三种情况,如下所示:

public class Out {
    
    int age = 10;

    class In{
    
     int age = 20;
     void method(){
    
            int age = 30;
         System.out.println(age);  //30
         System.out.println(this.age)  //20
         System.out.println(Out.this.age)  //10
     }
 }
}
  1. 对于外围类的成员变量,若无重名,直接在内部类的成员方法在中使用 变量名 访问即可,若有重名,则需要用 类名.this.变量名 访问
  2. 对于内部类且方法外的成员变量,若想在内部类的方法内访问,则需要 this. 来访问
  3. 对于内部类方法内的局部变量,直接用 变量名 访问即可,会采取就近原则。
  4. 因此,采取 外围类.this.变量名 访问外围类成员是万能且保险的方式。
  1. 内部类的成员方法中,去访问外部类的成员(普通或静态、私有成员)
    • 创建外部类对象,受访问权限控制。
  2. 外围类的普通成员方法,访问内部类的成员(普通和全局常量、私有成员)
    • 不能直接访问,需要创建内部类对象,使用 对象名.变量名 访问 ,不受访问权限控制
  3. 外围类的静态成员方法,访问内部类的成员(普通和全局常量、私有成员)
    • 要创建两个对象,先创建外围类对象,再创建内部类对象,使用 对象名. 访问,不受访问权限控制
  4. 外部类的普通成员方法,访问内部类的成员(普通和全局常量、私有成员)
    • 受内部类的访问权限控制
    • 创建两个对象,首先外围类对象,内部类对象
    • 通过对象名访问,受访问权限控制
  5. 外部类的静态成员方法,访问内部类的成员(普通和全局常量、私有成员)
    • 受访问权限控制
    • 创建两个对象,首先外围类对象,内部类对象
    • 通过 对象名.成员 访问

    规律:外围类和内部类相互访问,可以不受权限修饰符的访问限制,因为内部类相当于外围类的一个成员类,类似于成员变量的地位,相互不设防,但外部类就不行了。

//该形式适合在外围类中创建内部类
外围类名 对象名 = new 外围类构造函数();
内部类名 对象名 = 外围类对象名.new 内部类构造函数();

//简洁形式
外部类名.内部类名 对象名 = new 外围类构造函数().new 内部类构造函数();

//创建外围类
public class Out {
   

    int age = 10;

    //因为这里外围类已经存在,所以可以直接创建内部类对象。
    //当然也可以采用通用形式创建 内部类名 对象名 = 外围类对象名.new 内部类构造函数();
    //在静态方法里,不能这样创建(主方法也是静态方法)
    In in = new In();

    //外围类的普通成员方法
    void method2(){
   

        //3.外围类的普通成员方法,访问内部类的普通成员变量
        System.out.println(in.age);
    }

    //外围类的静态成员方法
    static void method3(){
   

        Out.In temp1 = new Out().new In();

        //4.外围类的静态成员方法,访问内部类的成员变量
        System.out.println(temp1.age);
    }

    //定义内部类
    class In {
   
        int age = 20 ;

        //内部类的普通成员方法
        void method1() {
   

            //1.在内部类的成员方法中,访问外围类的成员变量
             System.out.println(Out.this.age);

            External external = new External();

            //2.在内部类的成员方法中,访问外部类的成员变量
            System.out.println(external.age);
        }
    }
}

//定义外部类
class External {
   

    int age = 30;

    //外部类的普通成员方法
    void method4(){
   
        Out.In temp2 = new Out().new In();

        //5.外部类的普通成员方法,访问内部类的普通成员变量
        System.out.println(temp2.age);
    }

    //外部类的静态成员方法
    static void method5(){
   
        Out.In temp3 = new Out().new In();

        //6.外部类的静态成员方法,访问内部类的普通成员变量
        System.out.println(temp3.age);
    }
}

//在IDEA中,可将某个输出的age变量选中,所选中的age变量和对应被输出的age变量颜色相同,可以检验。
9.3.静态内部类
  • 静态内部类也是处在外围类成员位置的内部类,可以理解为是一个特殊的成员内部类,不同的是它需要使用static修饰。
9.3.1.概念
  • Oracle公司官网有一段文字解释静态内部类和成员内部类的区别:Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.
  • 解释:静态内部类相当于直接把一个类丢到另一个类中,明明可以独自做一个类,却偏偏要寄生在另一个类中,隐藏自己。
9.3.2.静态内部类的类加载机制
  • 静态内部类是独立存在于外围类中的
    • 创建外围类对象,不会触发静态内部类加载
    • 创建静态内部类对象,也不会触发外围类的类加载
    • 静态内部类创建对象不依赖于外围类
  • 类加载的过程对于内部类和外围类是完全独立的
9.3.3.修饰符和成员特点
  • 修饰符

    • 可以是public,protected,(default)缺省,private
  • 成员特点:

    • 成员变量、成员方法、构造器
  • 静态内部类可以创建普通类,可以创建所有成员,包括静态

  • 体现了静态内部类的独立性

  • 静态内部类只能继承一个静态内部类,而不能继承普通类

9.3.4.静态内部类的访问特点
  • 静态内部类内部访问外围类
    • 静态内部类只能直接访问外围类的静态成员,包括私有,直接 外围类名.成员 即可
    • 静态内部类如果想要访问外围类的普通成员,需要创建对象,使用 对象名.成员 即可。

      和一般类创建对象访问成员不同的是,静态内部类中创建外围类不受访问权限限制

    • 静态内部类中不存在一个外围类对象的引用,该对象完全可能不存在
    • 如果想要调用外围类中,和静态内部类同名的静态成员,只需要 外围类名.成员 即可
    • 如果想要调用外围类中,和静态内部类同名的普通成员,只需要创建外围类对象,对象名.成员 即可。

    静态内部类创建对象的时候,完全可能没有外围类对象,理解这一点和成员内部类的不同,就好理解上述现象了。

  • 外围类访问静态内部类成员
    • 如果访问静态内部类中的静态成员,可以直接 内部类名.成员,不受访问权限限制
    • 如果访问静态内部类中的普通成员,需要创建内部类对象,使用 对象名.成员 ,不受访问权限限制。
    • 在外围类的任何地方创建静态内部类对象,都可以用 静态内部类名 对象名 = new 静态内部类构造方法() 。最大的区别是不需要创建外围类对象,因为是相互独立的

    静态内部类是相对外围类独立的类,在外围类中访问静态内部类成员,除了不受访问权限限制外,和访问其他一般类并无差别

  • 外部类访问静态内部类成员(了解)
    • 创建静态内部类对象 外部类名.静态内部类名 对象名 = new 外部类名.静态内部类构造方法() 。和普通类对象访问成员一样,受访问权限限制
    • 访问静态成员,无需创建对象,直接 外部类名.静态内部类名.静态成员名 ,同样受访问权限限制。

    静态内部类在外部创建对象,可以完全独立于外围类对象,不会触发外围类的类加载

  • 静态内部类访问外部类成员(了解)
    • 静态成员,类名点直接访问
    • 普通成员需创建对象访问
    • 受访问权限控制

    在静态内部类中,访问外部类成员,和在普通类中访问其他类成员别无二致

9.4.局部内部类
9.4.1.概念
  • 局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内,将局部内部类看成是局部变量即可。
9.4.2.局部内部类特点:
  • 访问权限修饰符:局部内部类和局部变量一样,没有访问修饰权限,因为毫无意义,大括号已经限制了它的访问范围,局部内部类不能用static关键字修饰,原因和局部变量一样。

  • 成员特点:局部内部类内部无法定义静态成员,局部类中也没有静态的概念,其余变量和方法都是可以创建的,构造方法和普通类一致,用来给自身成员赋值。

  • 定义位置:几乎所有的局部位置都可以使用局部内部类,包括但不限于:

    • 方法
    • 代码块
    • if分支
    • for循环内部
    • 局部内部类的作用:当我们在局部位置,碰到了一个麻烦的问题,需要使用类来解决,但是又不希望这个类被外界知道,这种情况需要使用局部内部类
    • 局部内部类在使用前要有合适的理由,不然会自找麻烦
  • 访问特点:

    • 首先,局部内部类具有内部类的共同特点,可以无条件访问,外围类的所有成员
    • 但是局部内部类和局部变量一样,出了作用域就失效了。
      • 在外围类中无法创建对象,外部类更不行,只能在方法内部创建实例。
    • 要想使用局部内部类的功能,必须在该方法内部创建内部类对象,然后调用方法。
9.4.3.注意事项
  • Java8之前的版本中,局部内部类只能访问方法中加了final的局部变量
    • 局部内部类对象的生命周期和局部变量的生命周期是有冲突的,如局部变量和对象。
    • 为了解决冲突,JVM偷偷的帮我们把局部变量塞到了局部内部类对象的成员中了。
    • 但是问题仍然存在,将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的。
      • 也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变。
      • 最终Java开发者选择了妥协,使用final标记局部变量,这样就不能够改变它的值了。
    • Java8以后的版本,仍然也是这么做的,但是将final关键字隐藏在底层代码中了。
      • 也就是说,底层代码仍然会给局部变量加final。
      • 拥有局部内部类的方法的局部变量仍然是final修饰的。
      • 只不过从显式变成了隐式。
  • 局部内部类最大的优势是对方法外部完全隐藏,除了方法本身,即便是当前类也不知道它,不能访问它
    • 这是一种极致的封装思想的体现
//创建外部类
public class Outer {
   
    public void methodOuter(){
   
        //创建局部内部类
        class Inner {
   
            int num = 10;
            public void methodInner(){
   
                System.out.println(num);//10
            }
        }
        //只能在成员方法内部调用局部内部类
        Inner inner = new inner();
        inner.methodInner();
    }
}

public class Test {
   
    public static void main(String[] args){
   
        Outer obj = new Outer();
        obj.methodOuter;//10
    }
}

//一种局部内部类的经典封装思想
class Out{
   
    public IA getA(){
   
        class IAImpl implements IA{
   
            @Override
            public void test() {
   }
        }
        return new IAImpl();
    }
}
interface IA{
   
    void test();
}
9.5.匿名内部类
  • 是局部内部类的一种,是内部类的简化写法。本质是一个带具体实现的父类或者父接口的匿名的子类对象。开发中,最常用到的内部类就是匿名内部类。

  • 以接口举例,使用一个接口时,一般得做如下几步操作,

    1. 定义子类
    2. 重写接口中的方法
    3. 创建子类对象
    4. 调用重写后的方法

    匿名内部类可以对以上四个步骤进行简化

    前提:匿名内部类必须继承一个父类或者实现一个父接口。

//匿名内部类,相当于多态,父类 对象名 = new 子类。
接口名称 对象名 = new 接口名(){
   
    // 方法重写
    @Override
    public void method() {
   
    // 执行语句
    }
};

//匿名内部类的匿名对象,常作为某些函数的传入参数
new 接口名(){
   
    // 方法重写
    @Override
    public void method() {
   
    // 执行语句
    }
};
  • 以接口为例,体现匿名内部类的使用
public abstract class FlyAble{
   
    public abstract void fly();
}

public class InnerDemo {
   
    public static void main(String[] args) {
   
        /* 1.等号右边:是匿名内部类,定义并创建该接口的子类对象 2.等号左边:是多态赋值,接口类型引用指向子类对象 */

        FlyAble f = new FlyAble();//错误写法,不能new一个接口;


        //该写法实际上等同于 接口名称 对象名 = new 该接口的实现类名称(); 多态向上转型
        FlyAble f = new FlyAble(){
   
            public void fly() {
   
                System.out.println("我飞了~~~");
            }
            //此括号内容为该接口的实现类,只不过该实现类没有名称,故称为匿名内部类
        };

        //调用 fly方法,执行重写后的方法
        f.fly();
    }
}

注意事项:

  • 匿名内部类:在创建对象的时候,只能使用唯一一次,如果希望多次创建,则要单独定义实现类。
  • 匿名对象:在调用方法的时候,只能使用唯一一次,如果希望同一个对象多次调用方法,则必须给对象赋予名称
  • 匿名内部类省略了实现类/子类名称,而匿名对象省略的是对象名称
9.6.内部类的使用场景和缺点
9.6.1.内部类的使用场景
  • 场景一:无条件地访问外围类的所有元素(优点)
    • 无论是成员内部类、静态内部类、局部内部类还是匿名内部类都可以无条件访问
  • 场景二:隐藏类
    • 可以用private、protected修饰类
    • private修饰成员内部类、提供public的创建对象方法
  • 场景三:实现多继承
    • 可以创建多个成员内部类继承外部多个类
    • 然后创建内部类对象,实际上就是外围类继承了多个类的成员
  • 场景四:通过匿名内部类来优化简单的接口实现
9.6.2.内部类的缺点
  • 内部类的缺点也是显而易见,语法很复杂,在类中定义内部类也会导致类的结构变复杂,影响代码可读性,除此之外,不合理使用内部类还可能导致内存泄漏(了解),如果当内部类的对象被外围类以外的其他类引用时,就会造成内部类和外围类无法被GC回收的情况。