首页 > 编程语言 >Java内部类

Java内部类

时间:2023-10-24 21:13:07浏览次数:42  
标签:Java 内部 静态 void Main class Lambda

Java内部类详解

详细解释内部内的一些使用规则的原因

概览

定义:在一个类的内部定义的类。它的定义位于另一个类的内部,并且可以访问外部类的成员,包括私有成员

为什么要用

我觉得一个是为了符合OOP的封装原则,因为毕竟也可以直接把内部类函数和成员放到外面写。

另外就是既然可以写一个类,为什么要把它写在内部?原因是可能这个类的功能只能或者只需要被外部类使用。

还有一点:实现回调机制。Java没有函数指针和C#那种委托,但可以用函数式接口配合匿名内部类实现类似效果。Lambda表达式底层依然是会转换成匿名内部类的。

类型

静态内部类

可以看作外部类的静态成员,但是这个成员就像static Object一样,并没有初始化,如果他是public的,外界调用的时候是需要new的。

可以定义静态方法,普通成员等。只能访问外部类的静态变量和方法。

OuterClass.InnerClass inner = new OuterClass.InnerClass();

成员内部类

看作外部类的实例成员,使用他需要依靠外部类实例。

OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.innerMethod();

因为他不是静态的,所以内部不能定义静态成员和方法。 // 底层原因没弄明白,但是有大致猜测

Java语法:非静态代码块不能定义静态方法和变量

局部内部类

似乎没什么特色,就是在局部作用域实现的类。

public void outerMethod() {
        int outerData = 10;
        // 局部内部类
        class LocalInnerClass {
            public void innerMethod() {
                System.out.println("Inner Method: " + outerData);
            }
        }
        LocalInnerClass inner = new LocalInnerClass();
        inner.innerMethod();
    }

匿名内部类

通过继承父类或者实现接口的方式创建一个匿名内部类。Lambda底层也是转换成匿名内部类,只不过是只有一个方法的类。

InterfaceType obj2 = new InterfaceType() {
    @Override
    public void interfaceMethod() {
    }
};

ParentClass obj1 = new ParentClass() {
    @Override
    public void someMethod() {
    }
};

底层原理

结论

javap反汇编.class文件。

javap 参数

-c 查看JVM指令

-p 查看私有成员和函数

-v 输出行号、本地变量表信息、反编译汇编代码、当前类用到的常量池等信息。

jvm参数

-Djdk.internal.lambda.dumpProxyClasses 生成因为lambda表达式产生的内部类

注:PowerShell输入这个要用''引号括起来

提前说明,其实很多规定都是编译器的规定,如果硬要打破,是可以的(比如运行时用反射),因为JVM并没有反对这些规定,但由于大多数时候我们写代码然后运行,都不得不经过编译器检查,所以不得不遵循这些规则,当然这些规则并无害,只是学习的时候可能会产生很多疑惑:他为什么要这么规定?

内部类的各种访问规则很多,如果不了解底层很容易搞乱。

底层规则有以下几条,后面给出实例证明。

  • 内部类也会被单独编译成class文件,命名格式为OuterClass$InnerClass.class

  • 成员内部类访问外部类的私有成员

    • 成员和静态内部类:底层实现是:用外部类实例创建内部类的时候会把他的引用传递给内部类的构造函数(编译器生成),内部类就通过这个引用来访问,并且,能够访问私有成员(静态或非静态),编译器会生成对应的静态方法用于获取私有成员。这个解释适用于后面所有内部类。

    此处不明白为什么获取非静态的私有成员的时候,编译器要生产静态的方法,按理说非静态的也行。可能是为了统一吧,毕竟静态成员就是用的静态方法访问的,这样写格式统一,后面看到他生成的方法就知道了。

    另外,我一开始发现生成的静态方法后,想尝试直接手动调用,编译报错,看到网上解释说编译器不允许这么干。虽然这个方法编译时期已经是存在了的。Ref 隐藏的访问权限synthetic

    • 局部和匿名内部类:如果访问局部的变量,那变量必须是final或者等价于final的(即,局部内没有改变过)。底层实现:局部变量会直接作为值传递给这两种内部类的构造函数,赋值给内部的一个对应的final变量。

      如果访问的外部类的静态成员,则这个内部类必须是在非静态函数里面。

      总的来说:如果获取外部非静态变量,底层实现都是直接赋值给内部类中的final变量,而静态变量则是直接通过类来访问。

      其中,非静态变量分为局部变量和外部类的变量,局部变量必须为final或等同final,外部类的变量无此限制。

  • 静态内部类访问外部类成员:由于实例化的时候,静态内部类是通过外部类(而非实例)创建的,所以不会获得实例对象的引用,因此无法访问外部类的非静态成员。

  • 外部类访问内部类成员:

    • 静态成员:直接类名.成员,原理同上,编译器生成静态方法。

      注:内部类中只有静态内部类才能有静态成员。这是编译器规定的。原因不明,可能是防止语法二义性,详见 ref。或者就是一个浮于表面的解释:非静态代码块不能定义静态函数、成员。

      只有内部类才能是静态类。

    • 非静态成员:要先创建对象,然后对象.成员,原理同上。

  • Lambda表达式:底层会在外部类编译时创建对应的函数(如果表达式在静态函数内则创建静态函数,反之则反之),然后运行时会动态创建一个类。访问final局部变量,则变量直接传入,访问外部类的静态或者动态变量,会写在编译时再外部类创建的方法中,该方法的逻辑就是lambda表达式中的逻辑,而生成的类只是调用这个方法,顺便把局部变量传进去。

探究过程

全部源码如下,使用javac Main.java后使用java '-Djdk.internal.lambda.dumpProxyClasses' Main运行(cmd不用引号),可以生成全部需要分析的.class文件,其中运行时生成的时Lambda表达式相关的。

interface IAnoInner {
    void print();
}

public class Main {
    private static int privateStaticInt = 1;
    private int privateInt = 2;

    public static void testLambda(IAnoInner anoInner) {
        System.out.println(anoInner.getClass().getName());
        anoInner.print();
    }

    public void test() {
        // Lambda -> void lambda$test$0, Main$Lambda$2
        privateInt = 3;
        testLambda(() -> {
            System.out.println(privateInt);
        });
    }

    public static void main(String[] args) {
        // NoStatic Inner Class
        Main m = new Main();
        NonStaticInnerClass nsi = m.new NonStaticInnerClass();
        m.test();
        System.out.println(nsi.InnerNoStaticInt);

        // Static Inner Class
        System.out.println(StaticInnerClass.InnerStaticInt);
        System.out.println((new Main.StaticInnerClass()).InnerNoStaticInt);

        int localVarial = 1; // must be final or equals to final (not change)
        // localVarial = 2; // error, not equals to final

        // Local Inner Class -> Main$1LocalInnerClass
        class LocalInnerClass {
            public void print() {
                System.out.println(privateStaticInt + localVarial);
            }
        }
        LocalInnerClass lic = new LocalInnerClass();
        lic.print();

        // Anonymous InnerClass -> Main$1.class
        new IAnoInner() {
            @Override
            public void print() {
                System.out.println(privateStaticInt + localVarial);
            }
        };

        // Lambda -> void lambda$main$0, Main$Lambda$1
        testLambda(() -> {
            System.out.println(privateStaticInt + localVarial);
        });
    }

    public class NonStaticInnerClass {
        // private static int InnerStaticInt = 12;
        private int InnerNoStaticInt = 12;

        public void print() {
            System.out.println(privateInt + privateStaticInt);
        }
    }

    public static class StaticInnerClass {
        private static int InnerStaticInt = 12;
        private int InnerNoStaticInt = 12;

        public void print() {
            System.out.println(privateStaticInt);
        }
    }
}

所有.class文件:

Lambda 		: Main$$Lambda$1.class
Lambda 		: Main$$Lambda$2.class 
匿名内部类	: Main$1.class
局部内部类	: Main$1LocalInnerClass.class
局部内部类	: Main$1NoStaticLocalInnerClass.class
非静态内部类	:Main$NonStaticInnerClass.class
静态内部类	:Main$StaticInnerClass.class
Main.class
IAnoInner.class

使用javap -p反编译,下面分不同类型分析

Lambda

静态类中的Lambda只能访问静态成员或者局部变量,此处我访问的局部变量,他直接写入arg$1中了。

非静Lambda可以访问实例成员或者局部变量。因此后者会有一个Main的引用,前者没有,自然不能访问非静态成员。

// 静态类中的Lambda
final class Main$$Lambda$2 implements IAnoInner {
  private final int arg$1; // 局部变量
  private Main$$Lambda$2(int);
  private static IAnoInner get$Lambda(int);
  public void print();
}

// 非静态类中的Lambda 
final class Main$$Lambda$1 implements IAnoInner {
  private final Main arg$1; // Main的引用
  private Main$$Lambda$1(Main);
  private static IAnoInner get$Lambda(Main);
  public void print();
}

对应的,Main中为他们创建的方法,从static修饰符也能看出给谁的。

  private static void lambda$main$1(int);
  private void lambda$test$0();	// 非静

Lambda类会对应调用这俩。例如,(主要看注释)

  public void print();
    Code:
       0: aload_0
       1: getfield      #15                 // Field arg$1:LMain;
       4: invokespecial #26                 // Method Main.lambda$test$0:()V Lambda的逻辑写在了外部类的static方法中
       7: return

一开始我疑惑:为什么这里可以访问Main的私有方法呢?

后来顿悟:类的访问其实本没有界限,编译器插手的多了,也便有了界限,访问修饰符的检查和规定是编译器定的规矩,字节码层面其实是没有限制的。想怎么调怎么调,只不过我们自己写的代码会经过编译器检查,不允许这样访问。

注意:Lambda和匿名还有局部类的区别在于访问外部类的成员的方式,前者访问以及业务逻辑其实是在外部类自己的函数中,后两者是使用类生成的静态函数来访问,业务逻辑在内部类中。

  static int access$300();
  static int access$400(Main);
// Main生成的用于访问私有成员的函数

剩下的静态内部类和成员内部类的原理和上面说的一样。

访问私有成员核心就是编译器生成函数访问(Lambda例外)。

静态非静态成员访问核心就是类有没有实例引用。

局部变量就无脑final

Lambda表达式底层探究 ref1

Lambda表达式底层探究 ref2

标签:Java,内部,静态,void,Main,class,Lambda
From: https://www.cnblogs.com/BayMax0-0/p/17785752.html

相关文章

  • javaweb学习每日总结-第四天
    第四天学习Mybatis 今天在昨天大概学习完mybatis的概念之后,今天跟着案例敲了一边代码,自己亲自操作了一边数据库,过程相对比较顺利,下面我说说自己的感悟把,首先敲代码之前要配置好自己的mybatis.xml文件,然后创建Java类来写方法和对象,创建xml文件,然后用mapper接口将两个文件连接......
  • Java基础 缓冲流为什么能提高性能?
    缓冲流为什么能提高性能?知识点:1个字节=1B缓冲流自带长度为8192的缓冲区,字节缓冲流的缓冲区是byte类型的,是长度为8192的字节数组,为8K;而字符缓冲流的缓冲区是char类型的,是长度为8192的字符数组,为16K,因为 Java中一个字符占两个字节通过缓冲区可以显著提高字节流......
  • java复习
    内部类有哪些分类?在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。内部类的分类一般主要有四种:⚫成员内部类⚫局部内部类⚫匿名内部类⚫静态内部类静态内部类就是定义在类内部的静态类,静态内部......
  • Java基础 字符缓冲流
      字符流的基本流本身其实已经有缓冲区了,所以字符缓冲流提高的效率不是很明显。 字符缓冲流的构造方法:字符缓冲输入流:public BufferedReader(Reader r)  →  把基本流变成高级流字符缓冲输出流:public BufferedWriter(Writer r)  →  把基本流变成......
  • Java EasyExcel 随记
    JAR<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.7</version></dependency>入口EasyExcel.write(response.getOutputStream(),导出实体类.class).sheet("......
  • Java基础 字节缓冲流、字节缓冲流拷贝文件
    字节缓冲流:原理:底层自带了长度为8192的缓冲区。利用缓冲区可以一次读写8192个字节,从而提高性能public BufferedInputStream(InputStream is)  →  把基本流包装成高级流,提高读取数据的性能public BufferedOutputStream(OutputStream os)  →  把基本......
  • 【Java 进阶篇】JavaScript 自动跳转首页案例
    在这篇博客中,我们将创建一个JavaScript案例,演示如何自动跳转到网站的首页。这种自动跳转通常用于欢迎页面或广告页面等场景。我们将从头开始创建这个案例,逐步介绍相关的JavaScript知识,让初学者也能理解并实现这个功能。1.什么是自动跳转?自动跳转是指当用户访问一个网页时,页面会自......
  • 【Java 进阶篇】创建 JavaScript 轮播图:让网页焕发生机
    欢迎大家来到本篇博客,今天我们将一起探讨如何使用JavaScript创建一个精美的轮播图。轮播图是现代网站设计的关键元素之一,它能够使网页更加吸引人,提高用户体验。无需担心,本文将面向基础小白,从头开始解释每一步。我们将详细介绍如何构建一个轮播图,涵盖以下内容:什么是轮播图?创建HTML......
  • 【Java 进阶篇】JavaScript BOM(浏览器对象模型)详解
    BOM,即浏览器对象模型(BrowserObjectModel),是JavaScript与浏览器之间的接口,它允许JavaScript与浏览器进行交互,实现访问和控制浏览器窗口、文档和其他浏览器功能的功能。本文将详细介绍BOM的各个方面,包括窗口对象、定时器、历史记录、位置信息等,并提供示例代码来帮助您更好地理解和运......
  • 【Java 进阶篇】JavaScript电灯开关案例:从原理到实现
    JavaScript是一门强大的编程语言,它可以用来创建各种交互式网页应用。在这篇博客中,我们将通过一个简单的电灯开关案例来深入了解JavaScript的基础概念,包括HTML、CSS和JavaScript的结合使用。我们将从头开始构建这个案例,逐步引入相关概念,以帮助初学者更好地理解JavaScript的工作原理......