古法编程java学习之内部类

古法编程java学习之内部类
古法编程java学习之内部类

我的其他笔记可以查看JAVA学习记录总贴

内部类

基础知识

内部类的类的第五大成员。

五大成员分别是:属性、方法、构造器、代码块、内部类。

定义:一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为“内部类”,在外面的类称为“外部类”,内部类最大的特点就是可以访问私有属性,并且可以体现类与类之间的包含关系。

语法

class Outer{//外部类
    class Inter{//内部类
    
    }
}
class OtherOuter{//外部其他类
}

PS:内部类是OOP的重难点,底层源码有大量的内部类,必须要要下来这一块。

分类

  • 定义在外部类局部位置(比如方法内)

    • 局部内部类(有类名)

    • 匿名内部类(无类名,重点!!!!!!!)

  • 定义在外部类的成员位置上

    • 成员内部类(没用static)

    • 静态内部类(使用static)

class OuterClass {
    //成员内部类
    class MemberInnerClass {}
    //静态内部类
    static class StaticInnerClass{}
    //外部方法
    public void outerMethod() {
        //局部内部类
        class LocalInnerClass {}
​
        //匿名内部类
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类");
            }
        };
        runnable.run();
    }
}

局部内部类

细节

局部内部类定义在外部类的局部位置,比如在方法中,并且有类名。(代码块中也行,但罕见)

  1. 可以直接访问外部类的所有成员,包含私有的

  2. 不能添加访问修饰符,因为局部内部类本质上就是一个局部变量,局部变量不能使用修饰符,同理它也不能,但它可以使用final,因为局部变量也可以用final,顺带一提abstract也可以。

  3. 局部内部类是可以被继承的。

  4. 作用域:仅仅作用在定义它的方法或代码块中

  5. 局部内部类想访问外部类的成员,直接访问即可

  6. 外部类想访问局部内部类的成员,可以在作用域内实例化局部内部类,但是注意,必须在定义它之后再new

  7. 外部其他类不可能访问局部内部类,这个挺好理解,因为局部内部类本质局部变量,不在它作用域内。

  8. 如果外部类和局部内部类变量重名,则会遵循就近原则,优先访问到局部内部类的变量,如果想要访问外部类的成员,则用如下语法 外部类类名.this.成员名,ps:顺带一提,如果你不嫌脱裤子放屁,其实 外部类类名.this.成员名 这种语法,可以在类的所有地方精确调用到本类成员

  9. 再顺带一提,其实所有内部类都可以在内部继续写内部类,这也是它复杂的原因

class OuterClass {
    private int n1 = 10;
    private void m2() {}
​
    public void m1() {
        class InnerClass {
            public void show() {
                // 可以直接访问外部类的所有成员,包括私有类型,包括方法
                System.out.println("n1 = " + n1);
                m2();
            }
        }
        class A extends InnerClass{
        }
        InnerClass innerClass = new InnerClass();
    }
}

建议对着上面的代码,每一条细节都去自己实践一遍,看看违反了会报什么错。

匿名内部类

重点中的重点,这部分所有代码建议自己真的看完书去手敲一下,不要指望看一遍能懂,更不要单纯相信遇到的时候让AI来解释就好,没那么轻松。

特点

  • 本质是类,底层会有独立的class字节码文件

  • 属于内部类,定义在外部类/代码块,这类局部位置中

  • 没有类名,其实底层是有的,但是程序员不关心,因而匿名

  • 同时也是一个对象,类定义好的同时就已经被创建了

基本语法

new 父类/接口名(构造参数列表){
    // 类体:重写父类/实现接口的抽象方法,也可以新增自定义成员
};

基于接口的匿名内部类

class Outer04 {
    private int n1 = 10;
​
    public void method() {
        // 基于接口的匿名内部类
        // 1. 需求:使用IA接口,并创建对象
        // 2. 传统方法是写一个类,实现接口,创建对象,并调用方法
        // 3. 但如果我们的需求是,这个类只用一次,那么定义出来就有些浪费了,所以我们可以使用匿名内部类来简化开发
        // inner的编译类型是IA,而inner的运行类型是匿名内部类
        /*其实这里的底层含义是
            class Outer04$1 implements IA {
                @Override
                public void cry() {
                    System.out.println("匿名内部类实现了cry方法");
                }
            }
         */
        // 4. jdk底层创建了匿名内部类Outer04$1,然后创建了实例,并且把地址返回给inner
        // 5. 匿名内部类只能使用一次,不能再次使用
        IA inner = new IA(){
            @Override
            public void cry() {
                System.out.println("匿名内部类实现了cry方法");
            }
        };
        inner.cry();
        System.out.println(inner.getClass());
        IA inner1 = new IA(){
            @Override
            public void cry() {
                System.out.println("匿名内部类实现了cry方法");
            }
        };
        System.out.println(inner1.getClass());
    }
}
​
interface IA {
    public void cry();
}

值得说的细节在注释里都已经写明了,我在此补充一点

  • Outer04$1 就是 JVM 自动为第一个匿名内部类分配的名字,数字表示该类在外部类中出现的顺序。

基于类的匿名内部类

class Outer04 {
    private int n1 = 10;
    public void method() {
        Father fa = new Father("张三");
        Father fa1 = new Father("张三"){};
        // 证明匿名类创建的不是Father类,而是匿名内部类
        System.out.println(fa.getClass());
        System.out.println(fa1.getClass());
        Father fa2 = new Father("张三"){
            @Override
            public void show() {
                System.out.println("匿名内部类重写show()");
            }
        };
        fa2.show();
​
        Animal animal = new Animal() {
            @Override
            public void eat() {
                System.out.println("匿名内部类重写eat()");
            }
        };
        animal.eat();
    }
}
​
class Father {
    private String name;
    public Father(String name) {
        this.name = name;
    }
    public void show() {
        System.out.println("show()");
    }
}
​
abstract class Animal{
    public abstract void eat();
}

简单总结一些要点

  • 匿名内部类可以重写方法

  • 父类的构造器如果有参数,则你也要提供对应参数(如果有多个构造器,那你就提供符合其中一个的就行)

  • 基于抽象类(接口)的匿名内部类,必须实现其中所有抽象方法

一些细节

  • 匿名内部类,既有定义一个类的特性,也有创建对象的特性,是一个凉面派

  • 可以直接访问外部类的所有成员,包括外部私有成员

  • 不能添加访问修饰符(直觉看上去也没办法提交orz),因为它本质是局部变量

  • 作用域仅在定义它的方法or代码块中

  • 外部其他类也不能访问匿名内部类,因为其本质局部变量

  • 如果外部类和匿名内部类变量重名,则会遵循就近原则,优先访问到局部内部类的变量,如果想要访问外部类的成员,则用如下语法 外部类类名.this.成员名

匿名内部类练习

题目1:写一个基于接口的匿名内部类,并把它作为方法参数传入

public class AnonymousClass {
    public static void main(String[] args) {
        // 方式1:直接在参数位置编写匿名内部类
        f1(new AA() {
            @Override
            public void show() {
                System.out.println("匿名类实现接口");
            }
        });
        // 方式2:先创建匿名内部类对象,再传入参数
        AA aa = new AA() {
            @Override
            public void show() {
                System.out.println("匿名类实现接口");
            }
        };
        f1(aa);
    }
    public static void f1(AA aa){
        aa.show();
    }
}
​
interface AA{
    void show();
}

题目2:

  1. 定义一个铃声接口 Bell,接口中包含 ring() 方法。

  2. 定义一个手机类 Cellphone,类中包含闹钟功能方法 alarmclock(Bell bell),方法的参数类型为 Bell。

  3. 测试手机类的闹钟功能:通过匿名内部类创建 Bell 接口的实现对象,作为参数传入 alarmclock 方法,调用 ring() 方法时打印:懒猪起床了。

  4. 再传入另一个匿名内部类对象,调用 ring() 方法时打印:小伙伴上课了。

public class AnonymousClass {
    public static void main(String[] args) {

        new Cellphone().alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("懒猪起床了");
            }
        });

        new Cellphone().alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("小伙伴上课了");
            }
        });
    }

}

interface Bell {
    void ring();
}

class Cellphone {
    public void alarmclock(Bell bell){
        bell.ring();
    }
}

重点:一定要写一写感受一下,这部分涉及:多态、继承、动态绑定、内部类,多个知识点混杂在一起,需要认真练习

成员内部类

成员内部类定义在外部类的成员位置,并且没有static修饰

  1. 可以访问外部类的所有成员

  2. 可以添加任意的修饰符,因为其地位相当于一个成员

  3. 成员内部类的作用域和外部类的其他成员一样,都是整个类体。

  4. 成员内部类可以调用外部类的所有成员,包括私有。

  5. 外部类可以访问成员内部类的所有成员,包括私有成员,不过,必须要创建实例才能访问。

  6. 外部其他类想要访问成员内部类,有两种方法,分别标注在代码里了

  7. 如果外部类和内部类成员同名,则内部类访问时采取就近原则,如果一定要访问外部类的同名成员,则采用 外部类.this.成员名 的方式

public class AnonymousClass {
    public static void main(String[] args) {
        // 外部其他类访问成员内部类的两种方式
        // 方式一 通过外部类对象访问成员内部类对象,下面的两种写法本质上是等价的
        Outer.Inner inner = new Outer().new Inner(); // 写法1
        Outer outer = new Outer(); // 写法2
        Outer.Inner inner1 = outer.new Inner();
        // 方式二 在外部类中编写一个写法,返回成员内部类对象
        Outer.Inner inner2 = outer.getInner();
    }
}

class Outer{
    private int n1 = 10;
    private String name = "张三";

    public Inner getInner(){
        return new Inner();
    }
    // 成员内部类
    // 成员内部类是定义在外部类的成员位置上
    public class Inner{
        private int n2 = 20;
        public void say(){
            System.out.println("n1 = " + n1 + " name = " + name);
        }
    }

    public void show(){
        Inner inner = new Inner();
        // 即使是类内部的私有成员也可以被访问,因为本质上它也是类的一部分。
        System.out.println(inner.n2);
    }
}

静态内部类

静态内部类定义在外部类的成员位置,并且有 static 修饰符。

  1. 静态内部类可以直接访问外部类的所有静态成员(包括私有的),但是不能直接访问非静态成员;可以访问本类的所有成员,不管是静态还是非静态

    • 解释:

    • 静态内部类是外部类的一部分,因而可以访问外部类的所有静态成员。

    • 对于外部类的非静态成员,可以在静态内部类中实例化,然后访问。

  2. 可以添加任意访问修饰符,因为它的地位就是一个成员

  3. 作用域和其他的成员一样,是整个类体内部。

  4. 静态内部类可以在不依赖外部类的前提下被实例化

  5. 外部其他类访问静态内部类、静态内部类访问外部类、外部类访问静态内部类的逻辑都写在了代码里,如下所示

  6. 外部类和静态内部类成员重名时,静态内部类如果想要访问,则默认就近原则,如果想要访问外部类的同名成员,则需要 外部类名.成员

P.S. 内部类可以是静态的,但是顶层类不能是静态的。

public class AnonymousClass {
    public static void main(String[] args) {
        // 展示外部其它类访问静态内部类的方式
        // 方式1,直接new
        Outer.Inner inner = new Outer.Inner();
        inner.sayHello();
        // 方式2,编写一个方法,返回静态内部类的实例。
        Outer.Inner inner1 = new Outer().getInnerInstance("java");
        inner1.sayHello();
        // 方式2的补充,可以在外部类中写静态方法,返回静态内部类的实例,没有本质区别
        Outer.Inner inner2 = Outer.getInnerInstance_("HSP");
        inner2.sayHello();
    }
}

class Outer {
    private int n1 = 10;
    private static int n2 = 20;

    // 静态内部类
    public static class Inner {
        private String name;
        private static int count = 0;
        private static int n2 = 30;

        // 静态内部类可以有构造器
        Inner(String name) {
            this.name = name;
        }

        Inner() {
        }

        // 静态类里可以有普通方法
        public void sayHello() {
            // 展示静态内部类访问外部类的方式。
            // System.out.println(n1); 不能直接访问外部类非静态成员变量
            System.out.println(new Outer().n1); // 但是可以通过创建外部类实例来访问外部类非静态变量
            System.out.println(Outer.n2); // 可以直接访问外部类静态变量
            System.out.println(n2); // 直接访问遵循就近原则
            System.out.println("Hello, I'm " + this.name); // 也可以访问本类非静态变量
            System.out.println("Total count: " + count);  // 也可以访问本类静态变量
        }
    }

    // 接下来展示外部类如何访问静态内部类
    public void accessInner() {
        Inner inner = new Inner("HSP"); // 创建静态内部类对象
        System.out.println(inner.name); // 访问静态内部类成员变量
        System.out.println(Inner.count); // 访问静态内部类静态成员变量
    }

    public Inner getInnerInstance(String name) {
        return new Inner(name); // 创建静态内部类对象并返回给调用方
    }

    public static Inner getInnerInstance_(String name) {
        return new Inner(name); // 创建静态内部类对象并返回给调用方
    }
}

综合练习

练习1

当前代码会不会报错?为什么?
public class Test {

    class Inner {
        public int a = 5;
    }

    public static void main(String[] args) {
        Inner r = new Inner();
    }
}

答案:

会报错,因为成员内部类在创建时依赖外部类的实例而存在,需要一个外部类作为容器,传统初始化方法为 “外部类实例.new 成员内部类名”
main方法是静态的,静态方法中没有this关键字,因而报错,下面这样做就可以
public class Outer {
    public class Inner { }
    // 实例方法(非静态方法)
    public void show() {
        Inner inner = new Inner();  // ✅ 可以!
        // 等价于:this.new Inner();
        // 因为实例方法中,this 指向当前 Outer 对象
    }
}

练习2

下面这段代码会输出什么?
public class Test {
    public Test() {
        Inner s1 = new Inner();
        s1.a = 10;
        Inner s2 = new Inner();
        System.out.println(s2.a);
    }

    class Inner {
        public int a = 5;
    }

    public static void main(String[] args) {
        Test t = new Test();
        Inner r = t.new Inner();
        System.out.println(r.a);
    }
}

答案:

5
5

分析:

Test构造器里初始化的 S1 和 S2 分别是两个不同的成员内部类实例,而 main 方法里初始化的 r 也是一个不同的成员内部类实例。三个方法中的 a 是独立的

1 个帖子 - 1 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文