Java干货知识深入理解内部类

前言

说起内部类,大家并不陌生,并且会经常在实例化容器的时候使用到它。但是内部类的具体细节语法,原理以及实现是什么样的可以不少人都还挺陌生,这里作一篇总结,希望通过这篇总结提高对内部类的认识。

内部类是什么?

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

成员内部类

顾名思义,成员内部类是定义在类内部,作为类的成员的类。如下:

public class Outer {
public class Inner{
}
}

特点如下:

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

局部内部类

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

public class Outer{
public void test(){
class Inner{
}
}
}

特点如下:

  • 局部内部类不能有访问权限修饰符
  • 局部内部类不能被定义为static
  • 局部内部类不能定义static成员
  • 局部内部类默认包含了外部类对象的引用
  • 局部内部类也可以使用Outer.this语法制定访问外部类成员
  • 局部内部类想要使用方法或域中的变量,该变量必须是final的

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

匿名内部类

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

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

这是我们平时最常用的语法。

匿名内部类的特点如下:

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

嵌套类

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

public class Outer {
public static class Inner{
}
}

特点如下:

  • 嵌套类是四种类中唯一一个不包含对外部类对象的引用的内部类
  • 嵌套类可以定义static成员
  • 嵌套类能访问外部类任何静态数据成员与方法。

构造函数可以看作静态方法,因此可以访问。

为什么要有内部类?

从上面可以看出,内部类的特性和类方差不多,但是内部类有许多繁琐的细节语法。既然内部类有这么多的细节要注意,那为什么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.class和Outer$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.class和Outer$1Inner.class
将Outer$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后,程序不报错。

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

由上面可以看到,外部对象同样是被传入局部类中,因此局部类可以访问外部对象

嵌套类——>静态方法

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

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

匿名类可以看作是对前3种类的再次扩展。具体来说匿名类根据应用场景可以看作:

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

匿名类语法为:

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

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

声明一个匿名类:

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

这便是一个经典的匿名类用法。

同样编译上面代码会看到生成了两个.class文件Outer.class,Outer$1.class

将Outer$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出来了两个对象:outer和list,而在离开此方法时,我们只将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可以防止内部类内存泄露。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java内部类原理、概述与用法实例详解

    本文实例讲述了Java内部类原理.概述与用法.分享给大家供大家参考,具体如下: 内部类的概述 /* 内部类概述: 把类定义在其他类的内部,这个类就被称为内部类. 举例:在类A中定义了一个类B,类B就是内部类. 内部的访问特点: A:内部类可以直接访问外部类的成员,包括私有. B:外部类要访问内部类的成员,必须创建对象. */ class Outer { private int num = 10; class Inner { public void show() { //内部类可以直接访问外部类的

  • Java 中引入内部类的意义?

    前言 这是个好问题,因为它让我想起来自己刚学到内部类时候的"想用的冲动". 导致我代码里到处都是层层的内部类套嵌.不但经常搞得静态域错误一堆(内部类不允许有static成员字段),而且过一段时间自己都搞不清当初写的是什么. 一个很重要的设计准则是:设计是做减法,能不用模式就不用模式. 这个准则对内部类来说同样适用. 所以回答这个问题的基调应该是: 能不用内部类就不用内部类. 实践 我以前觉得内部类用来有针对性地暴露外部类的特定接口,比一下子把整个对象都给人家要好.比如说下面代码中的外部

  • java内部类原理与用法详解

    本文实例讲述了java内部类原理与用法.分享给大家供大家参考,具体如下: 概念 内部类:可以包含在另外一个类中的类 外部类:包含内部类的类 每个内部类都会被编译为一个独立的类,生成一个独立的字节码文件. 内部类可以方便地访问外部类的私有变量,内部类也可以声明为private从而实现对外完全隐藏. 分类 java中的四种内部类(根据定义的位置和方式划分) -静态内部类 -成员内部类 -方法内部类 -匿名内部类 分类介绍 -静态内部类 特征:在类的内部中存在另一个类,且该类被static修饰 使用范

  • Java内部类及其特点的讲解

    定义在类里面的类就叫做内部类. 内部类的特点: 在内部类中可以直接访问外部类的成员,包括私有的成员 在外部类中不能直接访问内部类的成员,必须通过创建内部类的对象来调用内部类成员 如何创建内部类对象: 内部类名 对象名= new 内部类名(); 对象名.成员名    但是私有化的东西还是不能访问的 可以修饰内部类的修饰符有哪些: private   static 如果通过private 修饰 ,只能在外部类中提供公共的方法对内部类进行访问 如果是static 修饰,如何创建外部类对象 外部类名.内

  • Java为什么匿名内部类参数引用需要用final进行修饰?

    事实上,除了匿名内部类参数,方法和作用域内的内部类内部使用的外部变量也必须是final 的.原因大致总结一下: 简单解释就是: 方法中的局部变量的生命周期很短,方法结束后变量就要被销毁,加上final是为了延长变量的生命周期. 进一步解释: 内部类通常都含有回调,引用那个匿名内部类的函数执行完了就没了,所以内部类中引用外面的局部变量需要是final的,这样在回调的时候才能找到那个变量,而如果是外围类的成员变量就不需要是final的,因为内部类本身都会含有一个外围了的引用(外围类.this),所以

  • Java干货知识深入理解内部类

    前言 说起内部类,大家并不陌生,并且会经常在实例化容器的时候使用到它.但是内部类的具体细节语法,原理以及实现是什么样的可以不少人都还挺陌生,这里作一篇总结,希望通过这篇总结提高对内部类的认识. 内部类是什么? 由文章开头可知,内部类的定义为:定义在另一个类或方法中的类.而根据使用场景的不同,内部类还可以分为四种:成员内部类,局部内部类,匿名内部类和静态内部类.每一种的特性和注意事项都不同,下面我们一一说明. 成员内部类 顾名思义,成员内部类是定义在类内部,作为类的成员的类.如下: public

  • Java基础知识汇总

    Java基础知识 1.Java语言的优点: 1)Java是纯面向对象语言 2)与平台无关性,一次编译到处运行 3)Java提供了狠多内置类库 4)提供了对web应用的支持 5)具有较好的安全性(数组边界检测.Bytecode检测)和健壮性(强制型机制.垃圾回收器.异常处理) 6)去除c++难以理解的一些特性(头文件 指针 运算符重载 多重继承) 2.java与c++的异同: 1)Java为解释型语言,c++为编译型语言,java会慢但是跨平台 2)Jave为纯面向对象,c++既面向对象又能面向过

  • Java 内存分配深入理解

    Java 内存分配深入理解 本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java.这类文章网上有很多,但大多比较零碎.本文从认知过程角度出发,将带给读者一个系统的介绍. 进入正题前首先要知道的是Java程序运行在JVM(Java  Virtual Machine,Java虚拟机)上,可以把JVM理解成Java程序和操作系统之间的桥梁,JVM实现了Java的平台无关性,由此可见JVM的重要性.所以在学习Java内存分配原理的时候一定要牢记这一切都是在JVM中进行的,JVM是

  • Java基础知识杂文

    1.基本概念 IO是主存和外部设备(硬盘.终端和网络等)拷贝数据的过程.IO是操作系统的底层功能实现,底层通过I/O指令进行完成. 所有语言运行时系统提供执行I/O较高级别的工具.(c的printfscanf,java的面向对象封装) 2.Java标准io回顾 Java标准IO类库是io面向对象的一种抽象.基于本地方法的底层实现,我们无须关注底层实现.InputStream\OutputStream(字节流):一次传送一个字节.Reader\Writer(字符流):一次一个字符. 3.nio简介

  • 详解Java基础知识——JDBC

    JDBC Java DataBase Connectivity,java数据库连接,为了降低操作数据的难度,java提供jdbc,按照java面向对象特点,对操作进行了很多封装. JDBC提供了很多接口,然后不同数据库厂商去实现这个接口,到底底层如何去实现,不同的数据库不一样,不同的数据库厂商需要提供接口实现类(驱动类.驱动程序 Driver.驱动) 我们连接不同的数据库,我们只需要使用不同的驱动即可. J:Java:提供访问数据库的规范(接口), DBC:接口的实现,厂商去实现这个接口. JD

  • JAVA多线程知识汇总

    线程概念 进程:启动一个应用程序就叫一个进程. 接着又启动一个应用程序,这叫两个进程.每个进程都有一个独立的内存空间:进程也是程序的一次执行过程,是系统运行程序的基本单位:系统运行一个程序即是一个进程从创建.运行到消亡的过程. 线程:线程是在进程内部同时做的事情,一个进程中可以有多个线程,这个应用程序也可以称之为多线程程序. 一个程序运行后至少有一个进程,一个进程中可以包含多个线程 线程调度: 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间. 抢占式调度:优先

  • Java基础知识总结之继承

    一.继承的基本概念 什么是继承呢? 我们可以想一下,既然是"继承",那么它必须是在两个或多个类之间所发生的关系吧,这样我们就可以说:这个类继承自什么类,假如只有一个类的话,它既无法继承别的类,而且也没有别的类来继承它,这就构不成什么继承关系了哈,理清楚了这一层关系之后我们再来谈谈它继承了什么呢? 既然说什么什么类继承自什么什么类,那它肯定要从继承的那个类中继承点什么呀!我们把发生继承关系的这两个类称为父类和子类,子类可以继承父类的属性和方法,这个就是继承的基本概念,下面我们来系统的学习

  • Java必备知识之位运算及常见进制解读

    目录 常见几种进制? Java八种按位运算? HashMap添加元素四步曲用到的位运算? 前奏:HashMap如何添加一个元素? 第一步曲 第二步曲 第三步曲 第四步曲 终曲:为什么HashMap底层源码用这么多位运算? 您好,我是贾斯汀,欢迎又进来学习啦! [学习背景] 学习Java的小伙伴,都知道想要提升个人技术水平,阅读JDK源码少不了,但是说实话还是有些难度的,底层源码实现的原理离不开各种常用的数据结构和算法,很多时候还会用到各种位运算,比如面试必问和工作写烂透了的HashMap,就一个

  • 详解Java中static关键字和内部类的使用

    目录 一. static 关键字 1. static修饰成员变量 2. static修饰成员方法 3. static成员变量的初始化 二. 内部类 1. 实例内部类 2. 静态内部类 3. 局部内部类 4. 匿名内部类 一. static 关键字 在Java中,被static修饰的成员,称之为静态成员,也可以称为类成员,其不属于某个具体的对象,是所有对象所共享的. 1. static修饰成员变量 static修饰的成员变量,称为静态成员变量 [静态成员变量特性]: 不属于某个具体的对象,是类的属

  • java String的深入理解

    java String的深入理解 一.Java内存模型  按照官方的说法:Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配. JVM主要管理两种类型内存:堆和非堆,堆内存(Heap Memory)是在 Java 虚拟机启动时创建,非堆内存(Non-heap Memory)是在JVM堆之外的内存. 简单来说,非堆包含方法区.JVM内部处理或优化所需的内存(如 JITCompiler,Just-in-time Compiler,即时编译后的代码缓存).每个类结构(如

随机推荐