Java内部类整理笔记——一篇读懂内部类

Java内部类整理笔记


一 . 内部类的总结

      一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以,可以认为内部类提供了某种进入其外围类的窗口。每个内部类都能独立地继承自一个接口的实现,所以无论外围类是否已经继承了某个接口的实现,对于内部类都没有影响。因为内部类提供了可以继承多个具体的或抽象的类的能力,使得设计与编程问题得以很好地解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说内部类允许继承多个类或抽象类。


二 . 内部类的定义

(1)内部类就是将一个类的定义放在另一个类的定义内部。其中,包含内部类的类被称为外部类(或者叫宿主类)。

(2)内部类定义语法格式如下:

public class OuterClass 
{
   
    //此处可以定义内部类
}


三 . 内部类的结构图

Java内部类整理笔记——一篇读懂内部类_第1张图片

内部类使用说明书:

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
  • 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能直接访问内部类的实现细节,例如内部类的成员变量。
  • 匿名内部类适合用于创建那些仅需要使用一次的类。
  • 内部类比外部类可以多使用三个修饰符:private 、protected 、static ——外部类不能使用这三个修饰符。
  • 非静态内部类不能拥有静态成员。

下面按照这个结构图,一一详细介绍内部类的基础知识。


四 . 非静态内部类

      成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员,分为两种:非静态内部类和静态内部类,没有使用 static 修饰的成员内部类是非静态内部类,有 static 修饰的成员内部类是静态内部类。因为内部类作为其外部类的成员,所以可以使用任意访问控制符如:private 、 protected 和public 等修饰。下面使用一个例子来分析非静态内部类的特点:

public class Person {

    private int age;
    //外部类的两个重载的构造器
    public Person(){}
    public Person(int age)
    {
        this.age=age;
    }
    //---------------------------------------------------------------------
    //定义一个非静态内部类
    private class Student
    {
        //非静态内部类的两个实例变量
        private String name;
        private String sex;
        //非静态内部类的两个重载的构造器
        public Student(){}
        public Student(String name,String sex)
        {
            this.name=name;
            this.sex=sex;
        }
        //下面省略 name 、sex 的 setter 和getter 方法
        //……
        //非静态内部类的实例方法
        public void info() {
            System.out.println("当前学生的名字是:" + name + ",性别:" + sex);
            //直接访问外部类的 private 修饰的成员变量
            System.out.println("当前学生的年龄是:" + age);
        }
    }
    //---------------------------------------------------------------------
    public void test()
    {
        Student student=new Student("张三","男");
        student.info();
    }
    public static void main(String [] args)
    {
        Person person=new Person(20);
        person.test();
    }
}
      上面程序中虚线中的部分就是一个使用 private 修饰的内部类,在外部类里使用非静态内部类时,与平时使用普通类差不多,首先实例化对象,再调用对象里的方法,如:Student student=new Student ("张三","男") ; student . info() ; 所示。
      编译上面程序,看到在文件所在路径生成了两个 class 文件,一个是 Person . class ,另一个是 Person$Student . class ,前者是外部类 Person 的 class 文件,后者是内部类 Student 的 class 文件,即成员内部类(包括静态内部类、非静态内部类)的 class 文件总是这种形式:OuterClass$InnerClass . class 。

      在非静态内部类里可以直接访问外部类的 private 成员,如上面程序中在 Student 类的方法内直接访问“当前学生的年龄”,即就是直接访问 Person 中的 age 变量。这是因为在非静态内部类对象里,保存了一个它所寄生的外部类的引用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在外部类实例里)。上面程序运行时的内存示意图如下:

Java内部类整理笔记——一篇读懂内部类_第2张图片

非静态内部类对象中保留外部类对象的引用内存示意图



五 . 使用 .this  

      当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在久使用该变量;如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,系统将出现编译错误:提示找不到该变量。因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用 this 、外部类类名 . this 作为限定来区分。下面通过代码来演示:

public class OuterClass {
    private String name="外部类的实例变量";
    private class InnerClass
    {
        private String name="内部类的实例变量";
        public void info()
        {
            String name="内部类里局部变量";
            //通过外部类类名.this.VarName 访问外部类实例变量
            System.out.println("外部类的实例变量:"+OuterClass.this.name);
            //通过 this.VarName 访问内部类实例的变量
            System.out.println("内部类的实例变量:"+this.name);
            //直接访问局部变量
            System.out.println("内部类里局部变量:"+name);
        }
    }
    public void test()
    {
        InnerClass innerClass=new InnerClass();
        innerClass.info();
    }
    public static void main(String [] args)
    {
        new OuterClass().test();
    }
}

      上面程序中通过 OutterClass.this.propName 的形式访问外部类的实例变量,通过 this.propName 的形式访问非静态内部类的实例变量。


六 . 使用 .New

      非静态内部类的成员可以访问外部类的 private 成员,但反过来就不成立。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。可以认为外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在。因此如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。下面通过代码来分析:

public class OuterClass {
    private int outer=3;
    //------------------------------------------------------
    private class InnerClass
    {
        private int inner=9;
        public void info()
        {
            //非静态内部类可以直接访问外部类的private成员变量
            System.out.println("外部类的实例变量:"+outer);
        }
    }
    //-----------------------------------------------------------
    public void test()
    {
        //外部类不能直接访问非静态内部类的实例变量,必须显示创建内部类对象
         System.out.println("内部类的inner值"+new InnerClass().inner);  
       
    }
    public static void main(String [] args)
    {
        new OuterClass().test();
    }
}

      Java规定,静态成员不能访问非静态成员的规则,因此,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中使用非静态内部类。同时,Java不允许在非静态内部类里定义静态成员,包括不能有静态方法、静态成员变量、静态初始化块。


七 . 静态内部类

      使用 static 修饰的内部类被称为类内部类,或是静态内部类。静态内部类属于外部类本身,而不属于尾部类的某个对象。静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。

     静态内部类是外部类的一个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等。但外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。下面用程序来说明:

public class OuterClass {
    static class InnerClass
    {
        private static int inner1=9;
        private  int inner2=10;
        public void info()
        {
           //通过类名访问静态内部类的类成员
            System.out.println(InnerClass.inner1);
            //通过实例访问静态内部类的实例成员
            System.out.println(new InnerClass().inner2);
        }
    }
}

     Java还允许在接口里定义内部类,接口里定义的内部类默认使用 public static 修饰,也就是说,接口内部类只能是静态内部类。如果为接口内部类指定访问修饰符,则只能指定 public 访问修饰符;如果定义接口内部类时省略访问控制符,则该内部类默认是 public 访问控制权限。


八 . 使用内部类

      定义类的主要作用就是定义变量、创建实例和作为父类被继承,定义内部类的主要作用也是如此。下面分三种情况讨论内部类的用法。

1 . 在外部类内部使用内部类                                                                                                                  

      在外部类内部类使用内部类时,与平常使用普通类没太大的区别。一样可以直接通过内部类类名来定义变量,通过 new 调用内部类构造器来创建实例。   

                     

2 . 在外部类以外使用非静态内部类

      在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用 private 访问控制权限,private 修饰的内部类只能在外部类内使用,对于使用其它访问控制修饰符的内部类,则能在访问控制符对应的访问权限内使用,

  • 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问。
  • 使用 protected 修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。
  • 使用 public 修饰的内部类,可以在任何地方被访问。

在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:

OuterClass . InnerClass . varName

     (如果外部类有包名,则还应该增加包名前缀。)

      由于非静态内部类的对象必须寄生在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法如下:

OuterInstance . new InnerConstructor ( )

下面看具体的程序:

class OutClass
{
    //定义一个内部类,不使用访问修饰符
    //即只有同一个包中的其他类可以访问该内部类
    class InClass
    {
        public InClass(String str)
        {
            System.out.println(str);
        }
    }
}
public class InnerDemo {
    public static void main(String [] args)
    {
        OutClass.InClass in=new OutClass().new InClass("内部类测试数据"); 
        /*
        上面代码可改为如下三行代码
        1.使用 OutterClass . InnerClass 的形式定义内部类
        OutClass.InClass in;
        2.创建外部类实例,非静态内部类实例寄生在该实例中
        OutClass out=new OutClass();
        3.通过外部类实例和 new 来调用内部类构造器创建非静态内部类实例
        in =out.new InClass("内部类测试数据");
         */
    }
   
}

      上面程序说明了,如果需要在外部类以外的地方创建非静态内部类的子类,则非静态内部类的构造器必须通过其外部类对象来调用。

      当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象。看下面的程序:

class SecClass extends OutClass.InClass
{
    //显示定义 SecClass 的构造器
    public SecClass(OutClass outClass) {
        //通过传入的 OutClass 对象显示调用 InClass 的构造器
        outClass.super("内部类子类测试数据");
    }
}

      从上面的代码可以看出,非静态内部类 InClass 类的构造器必须使用外部类对象来调用,代码中 super 代表调用 InClass 类的构造器,而 OutClass 则代表外部类对象。如果创建 SecClass 对象时,必须先创建一个 OutClass 对象。因为 SecClass 是非静态内部类 InClass 类的子类,非静态内部类 InClass 对象里必须有一个对 OutClass 对象的引用,其子类 SecClass 对象里也应该持有对 OutClass 对象的引用。当创建 SecClass 对象时传给构造器的 OutClass 对象,就是 SecClass 对象里 OutClass 对象引用所指向的对象。既然非静态内部类 InClass 对象和 SecClass 对象都必须持有指向 OutClass 对象的引用。那这两者之间的区别又是什么呢?区别是创建这两种对象时传入 OutClass 对象的方式不同:当创建非静态内部类 InClass 类的对象时,必须通过OutClass 对象来调用 new 关键字;当创建 SecClass 类的对象时,必须使用 OutClass 对象作为调用者来调用 InClass 类的构造器。


3 . 在外部类以外使用静态内部类

      因为静态内部类时外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:

new OuterClass . InnerConstructor ( )

     下面程序示范了如何在外部类以外的地方创建静态内部类的实例。

class StaticOutClass
{
    //定义一个静态内部类,不使用访问修饰符
    //即只有同一个包中的其他类可以访问该内部类
   static class StaticInClass
    {
        public StaticInClass()
        {
            System.out.println("静态内部类的构造器");
        }
    }
}

public class InnerDemo {
    public static void main(String [] args)
    {
        StaticOutClass.StaticInClass in =new StaticOutClass.StaticInClass();
        /*
        上面代码可改为如下两行
        1.使用 StaticOutClass.StaticInClass 的形式定义内部类变量
        StaticOutClass.StaticInClass in ;
        2.通过 new 来调用内部类构造器创建静态内部类实例
        new StaticOutClass.StaticInClass();
        * */
    }
}

      从上面代码中可以看出来,不管是静态内部类还是非静态内部类,它们声明变量的语法完全一样。区别只是在创建内部类对象时,静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。

     因为调用静态内部类的构造器时无须使用外部类对象,所以创建静态内部类的子类也比较简单,下面的代码就为静态内部类StaticInClass 类定义一个空的子类。

class StaticSecClass extends StaticOutClass.StaticInClass
{
    
}

      从上面代码中可以看出来,当定义一个静态内部类时,其外部类非常像一个包空间。

      相比之下,使用静态内部类比使用非静态内部类要简单得多,只要把外部类当成静态内部类的包空间即可。因此当程序需要使用内部类时,应该优先考虑使用静态内部类。


九 . 局部内部类

      如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因为局部内部类也不能使用访问控制符和 static 修饰符修饰。

      如果需要用局部内部类定义变量、创建实例或派生子类,那么都只能在局部内部类所在的方法内进行。下面程序创建了局部内部类如下:

public class InnerDemo {
    public static void main(String [] args)
    {
        //定义局部内部类
        class InnerClass
        {
            int a;
        }
        //定义局部内部类的子类
        class OneClass extends InnerClass
        {
            int b;
        }
        //创建局部内部类的对象
        OneClass oneClass=new OneClass();
        oneClass.a=5;
        oneClass.b=8;
        System.out.println("OneClass对象的a和b实例变量是: "+oneClass.a+oneClass.b);
    }
}

  编译上面程序,看到生成了三个 class 文件:InnerDemo.class 、 InnerDemo$1InnerClass.class 和  InnerDemo$OneClass.class ,这表明局部内部类的 class 文件总是遵循如下的命名格式:OuterClass$NInnerClass.class 。注意到局部内部类的 class 文件的文件名比成员内部类的 class 文件的文件名多了一个数字,这是因为同一个类里不可能有两个同名的成员内部类,而同一个类里则可能有两个以上同名的局部内部类(处于不同方法中),所以 Java 为局部内部类的 class 文件名中增加了一个数字。

      局部内部类是一个非常“鸡肋”的语法,在实际开发中很少定义局部内部类,这是因为局部内部类的作用域太小了:只能在当前方法中使用。大部分时候,定义一个类之后,当然希望多次复用这个类,但局部内部类无法离开它所在的方法,因此在实际开发中很少使用局部内部类。


十 . Java 8 改进的匿名内部类

      匿名内部类适合创建那种只需要使用一次的类,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。定义匿名内部类的格式如下:

new 实现接口 ( ) | 父类构造器 ( 实参列表 )
{

//匿名内部类的实体部分

 }

      从上面定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。

      关于匿名内部类还有如下两条规则:

  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类
  • 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。

      最常见的创建匿名内部类的方式时需要创建某个接口类型的对象,如下程序所示:

interface Student
{
    public String getName();
    public int getAge();
}

public class InnerDemo {
    public void InerTest(Student student)
    {
        System.out.println("该学生的姓名:"+student.getName()+",年龄:"+student.getAge());
    }
    public static void main(String [] args)
    {
        InnerDemo innerDemo=new InnerDemo();
        //调用 InerTest()方法时,需要传入一个 Student 参数
        //此处传入匿名实现类的实例
        innerDemo.InerTest(new Student() {
            @Override
            public String getName() {
                return "Jacky";
            }

            @Override
            public int getAge() {
                return 20;
            }
        });
    }
}

     上面程序中的 InnerDemo 定义了一个 InerTest ( ) 方法,该方法需要一个 Student 对象作为参数,但 Student 只是一个接口,无法直接创建对象,因此此处考虑创建一个 Student 接口实现类的对象传入该方法——如果这个 Student 接口实现类需要重复使用,则应该将该实现类定义成一个独立类;如果这个 Student 接口实现类只需使用一次,则可采用上面程序中的方式,定义一个匿名内部类。

     定义匿名内部类无须 class 关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象。由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或者接口里包含的所有抽象方法,下面是对于上面代码的另外一种实现方式:

interface Student
{
    public String getName();
    public int getAge();
}

class InnerStudent implements Student
{

    @Override
    public String getName() {
        return "Jacky";
    }

    @Override
    public int getAge() {
        return 20;
    }
}

public class InnerDemo {
    public void InerTest(Student student)
    {
        System.out.println("该学生的姓名:"+student.getName()+",年龄:"+student.getAge());
    }
    public static void main(String [] args)
    {
        InnerDemo innerDemo=new InnerDemo();
       
        innerDemo.InerTest(new InnerStudent());
        
    }
}

      上面两段代码功能完全一样,只不过采用匿名内部类的写法更加简洁。

      当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故 new 接口后的括号里不能穿入参数。但如果通过继承父亲来创建匿名内部类时,匿名内部类讲拥有和父类相似的构造器,此处的相似是指拥有相同的形成列表。看下面的程序:

abstract  class Student
{
    private String name;
    public abstract int getAge();
    public Student(){}
    public Student(String name)
    {
        this.name=name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

public class InnerDemo {
    public void InerTest(Student student)
    {
        System.out.println("该学生的姓名:"+student.getName()+",年龄:"+student.getAge());
    }
    public static void main(String [] args)
    {
        InnerDemo innerDemo=new InnerDemo();
        //调用有参数的构造器创建 Student 匿名实现类的对象
        innerDemo.InerTest(new Student("张三") {
            @Override
            public int getAge() {
                return 30;
            }
        });
        //调用无参数的构造器创建 Student 匿名实现类的对象
        Student student=new Student() {
            //初始化块
            {
                System.out.println("匿名内部类的初始化块……");
            }
            //实现抽象方法
            @Override
            public int getAge() {
                return 22;
            }
            //重写父类的实例方法

            @Override
            public String getName() {
                return "李四";
            }
        };
        innerDemo.InerTest(student);
    }
}

     上面程序创建了一个抽象父类 Student 类,这个抽象父类里面包含两个构造器:一个无参数的,一个有参数的。当创建以 Student 为抚慰的匿名内部类时,既可以传入参数,代表调用父类带参数的构造器;也可以不传入参数,代表调用父类无参数的构造器。当创建匿名内部类时,必须实现接口或抽象父类里的所有抽象方法。如果有需要,也可以重写父类中的普通方法。如上面程序中,匿名内部类重写了抽象父类 Student 类的 getName ( ) 方法,其中 getName ( ) 方法并不是抽象方法。

      在 Java 8 之前,Java 要求被局部内部类、匿名内部类访问的局部变量必须使用 final 修饰,从 Java 8 开始这个限制被取消了,Java 8 更加智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了 final 修饰。

interface Student
{
    void getAge();
    
}

public class InnerDemo {
    
    public static void main(String [] args)
    {
         int age=10;    //1⃣️
      Student student=new Student() {
          @Override
          public void getAge() {
              //在 Java 8 以前下面语句将提示错误,age 必须使用 final 修饰
              //从 java 8 开始,匿名内部类、局部内部类允许访问非 final 的局部变量
              System.out.println(age);
          }
      };
      student.getAge();
    }
}

      Java 8 将这种功能称为“ effectivity final ”  ,它的意思是对于被匿名内部类访问的局部变量,可以用 final 修饰,也可以不用 final 修饰,但必须按照油 final 修饰的方式来用——也就是一次赋值后,以后不能重新赋值。如上面程序如果在1⃣️代码后添加如下代码:age=20;将会导致编译错误。


你可能感兴趣的:(Java,内部类,匿名内部类,局部内部类,成员内部类)