【Java笔记】一网打尽Java中的内部类

一网打尽Java中的内部类


整体是在学习了《Java编程思想》和网上的一些资料进行笔记的总结,大致分为4个模块

  • 前提概要
    • Java类的分类
    • 外部类
  • 内部类
    • 内部类标识符
    • 成员内部类
    • 静态内部类
    • 局部内部类
    • 匿名内部类
  • 内部类扩展知识点(可能篇幅有点长…)
    • 成员内部类为什么可以直接访问其外围类的所有元素,包括私有成员?
    • 如何获得成员内部类的实例化对象?(其他类如何获得一个成员内部类的实例对象?)
    • 如何获得静态内部类的实例化对象?
    • 如何获得局部内部类的实例对象?
    • 内部类和外部类的元素出现命名冲突时怎么解决?
    • 非静态内部类为什么不能有静态元素,但是可以由常量(static final)类型?
    • 内部类的被继承和覆盖问题
    • 内部类的闭包和回调
    • 局部内部类(包括匿名内部类)访问的外部局部变量为什么必须是final?
    • 内部类中含有内部类以及特性的延续性
    • 接口的内部类
    • Java中有内部类的存在,那么Java允许有内部接口吗?

前提概要


Java中类的分类

在学习之前,我们先来了解一下类的种类,类分为外部类内部类,内外部类里面还要更小的划分。

外部类:

  • public类
  • 非public类

内部类:

  • 成员内部类(外部类中的内部类)
  • 静态内部类(外部类中的被static所修饰的内部类,又称嵌套类)
  • 局部内部类(外部类方法体或代码块中的内部类,甚至是方法中的作用域内)
  • 匿名内部类

外部类

我这里所说的外部类,是属于同一个Java文件下的同级类,可以分为两种,public类和非public类,它们之间谁也不是谁的内部类。同一个Java文件可以有多个同级类

  • public类
  • 非public类

因为在同一个Java文件下,Java只允许存在一个public类,既向外公开的类,而其他的非public类,不对外公开,一般用于辅助的角色,为public类服务。

  • public类,用于对包外的类公开,可以被包外的类通过导包的方式访问
  • 非public类,不对Java文件所在的包的外部公开,只允许被同一包下的类访问。
  • 同一个Java文件只允许定义一个public类,也就是我们最常使用到的外部类,剩余的都是非public类
  • 非public类只允许被abstract & final修饰,不能使用其他修饰符修饰

所以我们可以得出结论,不管是非public类还是public类,它们都是同级类,属于一个包下的类。但是非public是不对外公开的,所以只能内部调用访问。而public类是对外公开的,包外的类可以通过导包的方式来调用别的包的public类

补充:
接口也可以在一个类中定义,但是它的作用就类似于非public类,只能是一个非public的接口。

测试代码:
outerclass.pack1包下的Class1.java,里面有一个public的Class1类两个非public的A类和B

//outerclass.pack1包下
package com.snailmann.lean.learnclass.outerclass.pack1;

public class Class1 {

}

class A{

}

class B{

}

outerclass.pack1包下的Class2.java文件,我们可以在Class2.java文件的类中调用同一包下(outerclass.pack1)的Class1文件的public类(Class1类)和非public类(A类和B类)

//outerclass.pack1包下
package com.snailmann.lean.learnclass.outerclass.pack1;
public class Class2 {
	public static void main(String[] args){
		Class1 class1 = new Class1();
		A a = new A();
		B b = new B();
	}
}

这是outerclass.pack2包下的Class3.java文件,我们在outerclass.pack2包下的Class3.java文件的类中就无法访问到outerclass.pack1包下的非pulbic类(A类和B类)。但是我们可以访问公开的public类(Class1类)

package com.snailmann.lean.learnclass.outerclass.pack2;

import com.snailmann.lean.learnclass.outerclass.pack1.Class1;

public class Class3 {
	public static void main(String[] args) {
		Class1 class1 = new Class1();
		Class3 class3 = new Class3();
	}
}


内部类


内部类分为四种,在详细讲内部类的前提下,我们先来讲一下内部类标识符,引用至《Java编程思想》。

  • 成员内部类
  • 静态内部类
  • 局部内部类
  • 匿名内部类

内部类标识符

Java中,当源代码被编译的时候,每个类都会有一个对应的.class文件,其中包括了如何创建该类型对象的全部信息(此信息产生一个"meta-class",叫做Class对象)。内部类也是一个类,所以不例外的是它也会产生一个.class文件。不过内部类的class文件有着严格的命名规则。外部类名称+$+内部类名称

如果内部类是匿名的,编译器会简单的生成一个数字作为其标识符,如果内部类是嵌套在别的内部类的,只需直接将它们的名字加在其外围类标识符$的后面

public class OuterA {
	//内部类InnerB
	class InnerB {
	}
	public static void main(String[] args) {
		//匿名内部类
		new InnerB(){
		};
	}
}
	class文件名:	
	OuterA.class             //外部类A
	OuterA$InnerB.class      //内部类InnerB
	OuterA$1.class           //匿名内部类

说完内部类标识符后,我们再来了解一下四个内部类!

成员内部类

成员内部类就是在一个类中定义的一个非静态内部类,属于外部类的一部分,可以说是外部类的一个成员,成员内部类是我们通常所说的内部类。
特性:

  • 可以访问外部类的所有元素,包括私有元素
  • 该内部类对象隐式保存了一个引用,指向创建它的外部类对象
  • 成员内部类的内部不能有static元素(静态方法、静态代码块,静态变量,静态内部类等),但可以有static final编译时常量类型
  • 成员内部类可以被访问修饰符修饰(public,private...),也可以被abstract,final修饰,被static修饰则是静态内部类
  • 成员内部类的实例化依赖外部类对象的存在
  • 可以跟正常类一样实现单继承,多实现

测试代码:


//外部类
public class OuterClass {

	private String property = "OuterClass"
	
	//公有内部类
	public class InnerClassA {
		public void showMyOuter(){
			//访问外部类的私有成员变量property,本质是通过外部类对象引用
			System.out.println("My outerClass is " + property);
		}
	}
	
	//私有内部类
	private class InnerClassB {
	}
	
	//工厂方法返回一个私有内部类的实例
	public InnerClassB getInnerClassB(){
		return new InnerClassB();
	}
	
	public static void main(String[] args){
		OuterClass outer = new OuterClass();
		//实例化公有的成员内部类
		OuterClass.InnerClassA innerA = outer.new InnerClassA()
		innerA.showMyOuter();
		//私有的成员内部类不能通过上面的途径实例化,可以通过外部类的工厂方法来获得实例对象
		OuterClass.InnerClassB innerB = outer.getInnerClassB();
		
	}
}

静态内部类

静态内部类在《Java编程思想》中又称嵌套类,被static所修饰的成员内部类为静态内部类。如果声明了静态内部类,则意味着切断与外部类引用之间的关系,要创建一个静态内部类对象,也不再需要外部类对象的帮助。静态内部类在一定程度上可以当做一个与其外部类同级的类,只是定义在了外部类里边。

特性:

  • 静态内部类可以访问外部类的所有静态元素,但唯独不能访问外部类的非静态元素
  • 静态内部类内部可以拥有静态或非静态的元素,既什么都能拥有
  • 静态内部类可以被访问修饰符修饰(public,private...),也可以被abstractfinal修饰
  • 静态内部类的实例化不依赖外部类对象的存在,因为关联已经被static所切断
  • 可以跟正常类一样实现单继承,多实现

测试代码

public class OuterClass {

	private Static String property = "OuterClass";
	//定义一个静态内部类
	public static class InnerClass {

		String property = "InnerClass";
		
		public void showMyOuter(){
			//访问外部类的静态变量
			System.out.println("My outerClass is " + OuterClass.property );
		}

		public void showMe(){
			System.out.println("I am " + property);
		}
	}

	public static void main(String[] args) {
		//实例化一个静态内部类,不依赖外部类对象的存在
		OuterClass.InnerClass inner = new OuterClass.InnerClass();
		inner.showMe();
		inner.showMyOuter();
	
	}

}

局部内部类(local class)

局部内部类有3种:

  • 定义在代码块(静态,构造,局部)中
  • 定义在方法体内
  • 定义在方法体内的作用域中(比如if,for,while等语句内部)

使用局部内部类有两个用途:

  • 实现了某个类型的接口,于是可以创建并返回对其的引用
  • 要解决一个复杂的问题,向创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。

特性:

  • 可以访问外部类的所有元素,包括私有元素
  • 该内部类对象隐式保存了一个引用,指向创建它的外部类对象
  • 局部内部类不能有static元素(方法、代码块,变量,静态内部类),但可以有static final编译时常量类型
  • 局部内部类不能以被访问修饰符、static修饰,只能被abstract,final修饰
  • 想要在外部获得局部内部类的实例对象,只能通过局部内部类继承基类或实现接口,通过向上转型的方式,返回基类或接口的引用。
  • 可以跟正常类一样实现单继承,多实现

定义在代码块(静态,构造,局部):
我们首先来讲一下定义在代码块中的局部内部类,这种内部类的引用是无法别代码块外部的世界获取到了,同时该类的作用域是仅限于该代码块中,这个ClassA类在其他地方还可以定义名为InnerClassA的内部类。如果是定义在静态代码块,则不能使用外部类的非静态属性。

public class ClassA {
	private String property = "ClassA";
	//构造代码块
	{
		
		class InnerClassA {
			public void showMyOuter(){
				System.out.println(property);
			}
		}
		new InnerClassA().showMyOuter();
	}
	
	public static void main(String[] args){
		new ClassA();
	}
}

//output: ClassA
	

定义在方法体内:
以下的局部内部类与构造代码块中的就不一样了,该局部内部类是定义在外部类的方法中的。可以在方法中自行实现内部类的东西,就想代码块一样。但有一个不同的特性就是,方法中的内部类的引用是可以传递到方法外部的。实现原理是内部类实现一个接口, 然后该方法返回接口类型,从而实现外部获得向上转型而来的接口引用。

//非public接口
interface Interface{
	void show();
}

public class ClassA {
	private String property = "ClassA";
	//get内部类引用的方法,返回一个Interface类型
	public Interface getInnerClass(){
		//定义在方法体中
		class InnerClassA implements Interface
		{
			public void showMyOuter(){
				System.out.println(property);
			}

			@Override
			public void show() {
				System.out.println("I implement Interface in a method");
			}
		}
		//返回内部类实例,但方法返回值为Interface类型,所以向上转型为接口类型
		return new InnerClassA();
	}

	public static void main(String[] args) {
		Interface inter = new ClassA().getInnerClass();
		//获得向上转型的接口类型,调用被内部类重写的show()方法
		inter.show();
	}
}
//output:   I implement Interface in a method

定义在方法体内的作用域的就不列举的,与放在代码块中类似。


匿名内部类

匿名内部类的本质就是一个局部内部类,只是一个没有名字的局部内部类,所以特性参考局部内部类,它可以用来直接匿名实现一个正常类、抽象类或是接口。有一种创建一个匿名的类去实现一个接口或继承一个类的感觉。

特性:

  • 可以访问外部类的所以元素,包括私有元素
  • 该内部类对象隐式保存了一个引用,指向创建它的外部类对象
  • 匿名内部类不能有static元素(方法、代码块,变量,静态内部类),但可以有static final常量类型
  • 匿名内部类没有名称,无法被修饰,无法继承和实现。但本质是一个局部内部类
    测试代码:
abstract class AbsClass{
	public void show(){};
}
public class ClassA {
	public AbsClass getAbsClass(){
		//匿名内部类
		return new AbsClass (){
			@Override
			public void show(){
				System.out.println("I am a anonymous class");
			}
		}
	} 
}

小结:
  • 成员内部类和静态内部类其实都是一个成员内部类,属于类中类,只是静态内部类是被static修饰的成员内部类。
  • 局部内部类和匿名内部类其实都是局部内部类,匿名内部类是局部内部类的一种,区别只是匿名内部类是一个没有名称的类。
  • 成员内部类和局部内部类的区别就是,成员内部类处于外部类中,属于外部类的成员。而局部内部类是外部类的方法、代码块、控制流域等内部的类。区别就类似于成员变量和局部变量之间的关系。

注意:

  • 话说,我们注意到了内部类的存在,我们换个思维思考一下,是否有内部接口的存在呢?毕竟接口也可以算是一种特殊的抽象类,对吧。所以你先思想一会。在下面的扩展知识中,我会提到。

内部类的扩展知识


内部类的扩展知识比较多,所以我尽可能的把内部类的特性总结出来

一、成员内部类为什么可以直接访问其外围类的所有元素,包括私有成员?

因为当一个外围类对象创建一个成员内部类对象时,此内部类对象必定会秘密地捕获一个指向其外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择访问外围类的成员。所以就是通过外围类本身的引用来访问自己的私有成员,那当然是可以的啦。所以我们可以把一个成员内部类当做是外围类的一个成员。(构建内部类对象时,必须要一个指向其外围类对象的引用,如果编译器访问不到这个引用,就会报错。)

public class OuterClassA {

	private String property = "OuterClassA";
	public class InnerClass {
	
		public void showMyOuter(){
			System.out.println("My outerClass is " + property);
		}
	}

	public InnerClass getInnerClass (){
		return new InnerClass();
	}

	public static void main(String[] args) {
		OuterClassA classA = new OuterClassA();
		OuterClassA.InnerClass innerClass = classA.getInnerClass();
		innerClass.showMyOuter();
	}
}

我们发现property是外围类OuterClassA的私有变量。但是内部类的showMyOuter()方法却可以直接使用property属性。如上面所说,它的本质是通过外围类对象的引用来访问property的,但是我们从源代码是看不出来。这一切编译器已经帮我们都完成了,我们反编译一下生成的class文件。

public void showMyOuter()
{
   System.out.println("My outerClass is " + OuterClassA.this.property);
}

所以我们可以看到,本质的代码中是这样的,通过OuterClassA类当前对象的引用(OuterClassA.this)来访问property


二、如何获得成员内部类的实例化对象?(其他类如何获得一个成员内部类的实例对象?)

方式一:

public class OuterClassA {

	public class InnerClass {
		public void show(){
			System.out.println("I am InnerClass");
		}
	}
}

class OuterClassB {

	public static void main(String[] args) {
		OuterClassA classA = new OuterClassA();
		//OuterClassA.InnerClass innerClass = new OuterClassA.InnerClass();   //error
		OuterClassA.InnerClass innerClass = classA.new InnerClass();          //success
		innerClass.show();
	}
}
//错误信息:No enclosing instance of type OuterClassA is accessible. Must qualify the allocation with an enclosing instance of type OuterClassA (e.g. x.new A() where x is an instance of OuterClassA).

必须通过(外围类的对象.new 内部类)的方式实例化。要想创建内部类的对象,不能按照你想象的方式去创建,而是必须通过其外围类的对象来创建,因为成员内部类对象会暗暗地连接其外围类的对象,是相关联的(内部类可以直接访问外围类的所有元素,是有原因的),所以必须通过其外围类的对象来创建该内部的对象(既通过外对象来创建内对象
方式二:

public class OuterClassA {

	public class InnerClass {
		public void show(){
			System.out.println("I am InnerClass");
		}
	}
	//通过getInnerClass()方法返回一个内部类的引用
	public InnerClass getInnerClass (){
		return new InnerClass();
	}
}

class OuterClassB {

	public static void main(String[] args) {
		OuterClassA classA = new OuterClassA();
		OuterClassA.InnerClass innerClass = classA.getInnerClass();
		innerClass.show();
	}
}

通过工厂方法返回一个内部类的引用,通过外围类的方法来帮你实例化内部类。因为成员内部类在其外围类中,算是外围类的一个成员,所以实例化起来就没有这么的麻烦。直接new既可,而不需要用到.new语法。这种工厂方法通常会搭配私有的成员内部类来使用,因为私有成员内部类是无法通过第一种方式来实例化的。

所以这里的结论就是:
要获得一个成员内部类的实例对象的前提是,必须首先存在一个其外围类的对象,通过外围类的对象来构建内部类的对象,因为一个成员内部类的对象必须依赖其外围类对象的存在。


三、如何获得静态内部类的实例化对象?

要想在类2中获得一个类1的静态内部类的实例对象,相比要获得类1的成员内部类要容易的多。

//public类 
public class OuterClassA {
	//静态变量
	private static String property = "OuterClassA";
	
	public static class InnerClass {
	
		static final String property = "InnerClass";

		public void showMyOuter(){
			//通过类名.静态变量来访问外部类的静态变量
			System.out.println("My outerClass is " + OuterClassA.property);
		}

		public void showMe(){
			System.out.println("I am " + property);
		}
	}
}

//非public类
class OuterClassB{
	public static void main(String[] args) {

		OuterClassA.InnerClass inner = new OuterClassA.InnerClass();
		inner.showMe();
		inner.showMyOuter();
		//output:
		//I am InnerClass
		//My outerClass is OuterClassA

	}
}

由上,我们可以看出,我们不需要像获得成员内部类对象这么麻烦,要先实例化一个外部类对象,再通过外部类对象的.new语法来实例化内部类对象。静态内部类直接通过外部类.内部类 变量名 = new 外部类.内部类()的方式即可。因为静态内部类的实例对象不再需要与外部类对象关联在一起,所以我们就可以直接new一个内部类对象。同时因为无关联,所以静态内部类是无法访问到外部类的非静态成员的。


四、如何获得局部内部类的实例对象?

想要在外部获得一个类的局部内部类的实例对象,恐怕只有定义在方法体内的实例对象能返回出去,并且是通过向上转型的方式。这也是局部内部类常被运用的方式

//非public抽象类
abstract class AbsClass{
	public abstract void show();
}
//非public接口
interface Interface {
	void show();
}

public class OuterClass {
	//获得向上转型后的AbsClass引用
	public AbsClass getAbsClass(){
		class InnerA extends AbsClass{
				@Overrde
				public void show(){
					System.out.println("I am InnerA");
				}
		}
		return new InnerA();
	}
	//获得向上转型后的Interface引用
	public Interface getInterface(){
		class InnerB implement Interface{
			@Override
			public void show(){
				System.out.println("I am InnerB");
			}	
		}
		return new InnerB();
	}

	public static void main(String[] args){
			OuterClass outer = new OuterClass;
			AbsClass innerA = outer.getAbsClass();
			Interface innerB = outer.getInterface();

			innerA.show();
			innerB.show();
			//output:
			//I am InnerA
			//I am InnerB
	}
}


可以看出局部内部的实例对象实际是通过工厂方法返回的,原理是通过类的向上转型的机制传递到外部。这是定义在方法体内的局部变量,要是其他两种局部内部类,我估计是无法无法传递到外部的了。


五、内部类和外部类的元素出现命名冲突时怎么解决?

我们这里以成员内部类成员变量冲突来举例子
未冲突例子:

public class OuterClass {

	//外部类的property变量
	private String property = "OuterClass";
	

	public class InnerClass {
		public void showMyOuter(){
			//引用外部类的property变量
			System.out.println("My outerClass is " + property);
		}
	}
}

我们可以看到在没同名变量冲突前,我们在内部类的方法里是可以直接使用外部类的变量的,不需要附加任何的东西。但是呢,这只是我们在表面上看到的,当我们反编译去查看这段代码时,我们就会发现,编译器已经帮我们做了很多的工作。

//反编译的代码
public void showMyOuter(){
	System.out.println("My outerClass is " + OuterClass.this.property);
}

实际的代码是通过外部类名.this.外部类变量名来访问的。(这也就是我们常说的,内部类对暗暗的获取一个外部类对象的引用。)

冲突例子:

//外部类OuterClass
public class OuterClass {

	//外部类的property变量
	private String property = "OuterClass";
	
	//内部类InnerClass
	public class InnerClass {
		
		//内部类的property变量
		String property = "InnerClass";

		public void showMyOuter(){
			//访问外部类的property变量
			System.out.println("My outerClass is " + OuterClass.this.property);
		}

		public void showMe(){
			//访问内部类的property变量
			System.out.println("I am " + property);
		}
	}
}

如果存在冲突的例子,那么解决这个冲突的工作就不能再交给编译器去处理的。我们需要自己把代码补全。访问外部类变量的完整方式是OuterClass.this.property(外部类.this.变量名)


六、非静态内部类为什么不能有静态元素,但是可以由常量(static final)类型?

这里的非静态内部类说的就是成员内部类和局部内部类和匿名内部类

public class OuterClass {

	private String property = "OuterClass";
	public class InnerClass {
		static final String property = 1;

		public void showMyOuter(){
			System.out.println("My outerClass is " + property);
		}
	}

}

第一个小问:
首先解释第一点为什么非静态内部类不能含有静态元素,首先我们明确三个知识点前提

  • 类加载时,静态成员优先于非静态成员加载
  • 内部类也算是外部类的一个成员
  • 内部类的加载依赖外部类实例对象的创建

明确了三个知识点的前提下,这个为什么就很好理解了。第一,静态成员优先加载,所以当我使用到OuterClass类时,JVM会加载这个类,首先加载静态成员,然后创建对象时加载实例成员。第二,内部类的对象的创建依赖外部类对象,所以必须等外部类静态和非静态成员加载完毕后,才开始有机会加载内部类。如果非静态内部类中含有静态成员吗,那么外部类的非静态成员都已经加载完毕了,内部类却还有静态成员没加载,这就违背了同一个类中静态成员优先于非静态成员加载的顺序(内部类也是外部类的一个成员)。所以非静态内部类不能有静态元素。为什么静态内部类就可以有呢?因为静态内部类已经不依赖外部类对象了。

第二个小问:
然后我们再来解释为什么加了final就可以通过编译了呢?其实并不是完全是这样的

public class OuterClass {

	private String property = "OuterClass";
	public class InnerClass {
		static final int number = 123;                         //no error 
		static final String property = "InnerClass";           //no error 
		
		static final String str = new String ("abc");          //error
		static final Integer integer = new Integer(10);        //error
		static final Integer integer = 123;                    //error
		//The field integer cannot be declared static in a non-static inner type, unless initialized with a constant expression

	}

}

如上代码我们可以看到,凡是赋予字面量的都通过,通过new实例对象的方式都不能通过。但是也有个例外,那就是Integer integer = 123;这里为什么也报错呢,这种方式编译后,我们看编译器生成的代码就知道了。Integer integer = 123 的编译后的代码是Integer integer = Integer.valueOf(123), 点进valueOf方法,我们可以看到返回的效果也就等于new Integer()或取常量池中缓存对象。说白了就是还要在运行期间来加载Integer这个类,执行这个类相应的方法才能赋值,而不是在编译器就已经知道了integer123

static final int number = 123;static final String property = "InnerClass";的方式是相等于定义了一个编译时常量,是不需要加载类就可以访问到的,是编译期间就已经确定了的,写在了静态常量池中。

所以我们知道了,配合static final定义出编译期常量是不报错的,因为这个static final的编译器变量的使用压根就不会导致类的加载,也不需要类的加载就完成赋值了。自然就不会有顺序冲突。但static final的非编译期常量会导致类的加载,所以会报错。

对于这个问题,可能需要更深入的了解编译期常量和非编译器常量的加载机制才能更好的理解清楚。再没清楚之前,我们可以把这个当做是一个标准来使用即可。

为什么非静态内部类中不能有static属性的变量,却可以有static final属性的变量?


七、内部类的被继承和覆盖问题

第一小问:内部类能被继承吗?
内部类能正常的去实现单继承和多实现。那么内部类能被别的类所继承吗?当然可以,但是这个继承的方式跟正常类继承有些不一样。
因为内部类的构造器必须连接指向其外部类对象的引用。既内部类的实例化依赖其外部类对象的存在,必须由外部类对象来构建内部类对象。所以当一个继承了内部类的子类实例化时,子类实例化必然导致其父类先实例化,但这个父类(内部类)的实例化又必须依赖它的外部类对象的存在。所以这关系就非常复杂了,必须要用特殊的语法来说清这个关系。我们看测试代码:
测试代码:

//外部类
class Outer {
	内部类(父类)
	class Inner{
	}
}

//继承于一个内部类,是Outer.Inner类的子类
public class InheritInner extends Outer.Inner{

	//所以子类的构造器必须通过特殊的语法来说清他们之间的关系
	//子类构造器,必须传入父类的外部类的对象,通过外部类对象.super()来理清这个关系
	public InheritInner(Outer outer) {
		outer.super();
	}

	public static void main(String[] args) {
		InheritInner inheritInner = new InheritInner(new Outer());
	}

}

我们看到,我们必须在继承于内部类的子类构造器中传入一个其父类的外部类的对象。并通过外部类的对象.super()方法去实例化内部类,从而解决父类要先实例化的问题。如果不使用这个特殊语法outer.super(),那么就会编译器报错提示

No enclosing instance of type Outer is available due to some intermediate constructor invocation

这就是继承内部类需要解决的问题

第二小问:内部类能被覆盖重写吗?
我们首先来想象这么一个场景。如果一个类(子类)继承了一个含有内部类的外部类(父类),并重写了父类的内部类,那么父类的内部类会被子类重写的内部类所覆盖吗?
我们这里引用回《Java编程思想》书中的例子,看代码:

class Egg {
  private Yolk y;
  protected class Yolk {
    public Yolk() { System.out.println("Egg.Yolk()"); }
  }
  public Egg() {
    print("New Egg()");
    y = new Yolk();
  }
}	

public class BigEgg extends Egg {
  public class Yolk {
    public Yolk() { System.out.println("BigEgg.Yolk()"); }
  }
  
  public static void main(String[] args) {
    new BigEgg();
  }
} 

结果是:

New Egg()
Egg.Yolk()

发现输出的依然是父类内部类的实现方法。而不是子类重写后的内部类的实现方法。这说明并不会发生覆盖。只是两个类中自己的内部类,这个两个内部类是两个完全独立的实体,各自在自己的命名空间内。当然你想重写方法到是可以的。在子类的内部类中继承于父类内部类,并重写方法即可(注意,构造函数不能被重写)


八、内部类的闭包和回调

在讲内部类的闭包和回调之前,那你必须先了解一下什么是闭包对吧。我首先说一下《Java编程思想》里闭包的概念,再放几个链接交叉参考一下,由于我本身也可能存在理解不到位的地方,如果有误,那么…打扰了。

书中的定义:
在面向对象语言中,我们可以说闭包(closure)是一个可调用对象,它记录了一些信息,这些信息来自创建它的作用域。
(这个东西有些抽象,不好理解,可能需要结合代码理解,)

参考链接:

链接中的定义,我这里引用@胖胖的一句话:

闭包的定义很好理,有俩:

  • 一个依赖外部环境自由变量的函数;
  • 这个函数能够访问外部环境的自由变量;

我们再来看一下这段代码

class Foo {  
    private int x;  
    public int AddWith( int y ) { return x + y; }  
}  

【R大原话】这样的AddWith()有一个参数y和一个自由变量x,其返回的值既依赖于参数的值也依赖于自由变量的值。为了让AddWith()正确工作,它必须依附于Foo的一个实例,不然就得不到x的值了(称为:“变量x未与值相绑定”)。 这个AddWith()函数就可以说是一个闭包 。当然严格来说方法所捕获的自由变量不是x,而是this;x是通过this来访问到的,完整写出应该是this.x。

结合理解:
怎么形成闭包,形成闭包的途径就是一个内部作用域带有外部环境的某些信息。在《Java编程思想》中,作者将内部类形容为面向对象语言中闭包。因为内部类含有外围类对象的引用(外围类就是创建它的作用域,也即是外部环境)(而内部类含有外围类对象的引用等于含有外部环境的某些信息,也可以说是全部信息),所以综合理解,我们就能知道内部类可以作为外部类的一个闭包存在。

内部类闭包回调的运用:
好,当我们理解了什么是闭包时,我们就正式的说一下内部类作为闭包的作用:
我们来想象这么一个场景,当类和接口,接口和接口的方法有了冲突时,而这个冲突又不允许通过修改接口或类去解决,但是又必须继承或实现这两个产生冲突的方法。闭包回调就产生作用了。


class Writer {//作家基类
 void work(){};
}
interface Programmer{//程序员接口
 void work();
}

public class Person extends Writer {
	
	 @Override
	 public void work(){
	   //写作
	   System.out.println("写作");
	 }
	 public void code(){
	   //写代码
	   System.out.println("编码");
	 }
	
	//内部类作为闭包
	 class Closure implements Programmer{
	   @Override
	   public void work(){
	     code();
	   }
	 }

	 public static void main(String[] args) {
		 Person person = new Person();
		 Person.Closure closure = person.new Closure();
			
		 person.work();
		 closure.work();
	}
}

我们这里有个人即是作家,也是程序员,由于继承作家类的同时实现接口,会引起work方法的歧义,到底work()是写作,还是在编码?但我们又必须要用到类和接口在代码层面实现这个人。我们可以通过这种曲线救国路线去实现。

首先定义一个Person类并继承了作家类Writer,重写了work()用于写作,同时有自己的实现方法code()用于编码。在Person内定义一个内部类Closure并实现程序员接口Programmer,重写接口的work()方法,调用外围类的code ()方法(这个在内部类方法里调用的code()称为回调方法)。这样我们就能既继承类实现接口,还能分别调用他们的work()方法去写作或者编码.

Person是外部环境,Closure类是内部环境,内部环境中可以访问到外部环境的code()方法,所以…这是一个闭包

当然我这里的场景可能不能很好的解释闭包回调的作用,毕竟没有多实用,但是足以说明有这么一个作用。想更好理解,那还是要点…社会经验吧


九 、局部内部类(包括匿名内部类)访问的外部局部变量为什么必须是final?

前提声明
Java8之前,在局部内部类使用了的外部的局部变量,这个外部局部变量必须用final修饰。Java8之后,即使匿名内部类使用到了外部的局部变量,没有写final,是可以通过的,但前提是这个局部变量没有被修改赋值过。在能编译通过的情况下,即使你没有加final,最后编译器是会帮你把final加上的。

为什么???:

继续说,我这里的观点主要还是引用@胖胖大佬所说的东西,大家可以直接看胖胖大佬所说的可能会更加直观,且没有歧义。但毕竟博客是写给自己的总结,所以我还是要冗余的说一下

interface Interface {
	void eat();
}

public class OuterClass {
	//food为形参,也属于局部变量
	public Interface getInterface(final String food){
		//方法中的局部变量
		final private String property = "getInterface";
		//匿名内部类
		return new Interface() {
			@Override
			public void eat() {
				System.out.println("I eating " + food);
			}
			
			public show (){
				System.out.println(property + " method running");
			}
		};
	}

}

我们可以看到这个匿名内部类是一个典型的闭包,因为匿名内部类中引用了外部环境的信息,将外部环境的局部变量food和porperty变量封装在内部类中,形成一个闭包。但是为什么要用final去修饰呢?这是因为Java编译器支持闭包,但是支持的不太完整。@胖胖的原话差不都是因为编译器编译时会悄悄的对函数做手脚,偷偷的将外部的food变量和property变量,拷贝到匿名内部类中如下代码所示

interface Interface {
	void eat();
}

public class OuterClass {

	public Interface getInterface(final String food){
	
		final private String property = "getInterface";
		//匿名内部类
		return new Interface() {
			//编译器将要访问的外部自由变量深拷贝一个副本到匿名函数中
			String copyFood = new String(food);
			String copyProperty = new String(property);
			@Override
			public void eat() {
				System.out.println("I eating " + food);
			}
			
			public show (){
				System.out.println(property + " method running");
			}
		};
	}

}

如@胖胖和@R大所说

Java编译器实现的只是capture-by-value,并没有实现capture-by-reference。

而只有后者才能实现保持局部内部类和外部环境局部变量的同步,因为是深拷贝,所以当外部环境局部变量被修改时,是无法同步到局部内部类里面的,这就容易导致数据错误的问题。
但Java又不肯明说,只能暴力一刀切,既然内外还无法实现同步,那么不允许大家改变外部环境的局部变量,所以就不存在同步问题了。

。------------------------------------------------------------------------------------------------------------------------------------------------------------。

以为上我之前看到的解释,后面我看一个视频的时候,发现另一个更为通俗易懂的解释,结合上面的说明就能更加的简单易懂。

public static void main(String[] args){
	
	//list变量指向ArrayList对象
	public List<String> list = ArrayList<>();
	
	new Thread(){
		public void run(){
			List<String> Strlist = list;
		}
	}
}

理由是这样的:

  • 第一,Java中的传递是值传递,我们传递的参数是ArrayList对象,ArrayList对象是一个引用类型
  • 第二,我们要将list变量指向的ArrayList对象传递到匿名类中
  • 第三,匿名类得到了list变量所指向的ArrayList对象,并赋值给了strList变量
  • 第四,如果外部的list变量发生了修改,指向了另外一个ArrayList对象,此时就会出现一个问题,外部的list所指向的ArrayList对象和内部的strList变量所指向的ArrayList已经不是同一个了,所以这会造成数据不一致的问题

十、内部类中含有内部类以及特性的延续性

静态内部类中才允许内嵌静态内部类
成员内部类中的内部类也延续成员内部类的特性,可以被访问修饰符修饰等
局部内部类的中的内部类延续局部内部类的特性,最多只能实现局部内部类允许有的特性
匿名内部类的本质就是一个局部内部类,所以与局部内部类一样,其内部类最多只能实现局部内部类允许有的特性


十一、接口中的内部类

接口中的很多特性,比如接口的默认方法为public abstract,变量为public static final。而接口中内部类呢?接口中的内部类也有特殊的地方,那就是默认被public static 修饰,既接口中的内部类都是静态内部类。

public interface Interface {
	//接口的静态方法
	static void showMy(){System.out.println("I am Interface");}
	
	//接口中的方法默认为public abstract
	void show();
	
	//接口中的内部类为public static
	class InnerClass implements Interface{
	
		@Override
		public void show() {
			//内部类可以调用外部的静态方法,所以这也证明了接口的内部类为静态内部类
			showMy();
		}
	}

}


十二、Java中有内部类的存在,那么Java允许有内部接口吗?

答案是…允许的。Java内部接口只允许有三种:

  • 在接口中定义的内部接口
  • 在外部类(top-level class)的定义的内部接口
  • 在静态内部类内部定义的内部接口

我说了内部接口只允许定义在以上三种情况,那么我们来尝试一下三种情况外的定义,例如局部内部类中定义

public class OuterClass {
	//代码块
	{   //局部内部类
		class InnerA {
			interface Interface{}         //编译错误,error
		}	
	}
}

我们会得到一个编译报错,提示我们成员接口只能被定义在一个顶级类,接口或是静态上下文中。我们来解释一下,这个顶级类就是最外层的外部类。接口就不说了,静态上下文就是一个静态内部类。

The member interface Interface can only be defined inside a top-level class or interface or in a static context

测试代码:

import com.snailmann.lean.learnclass.innerInterface.OuterClass.InnerA.Interface2;
import com.snailmann.lean.learnclass.innerInterface.OuterClass.InnerA.Interface2.Interface3;


public class OuterClass {
	//顶级类的内部接口
	interface Interface1 {}
	
	//静态内部类可以实现顶级类的内部接口
	public static class InnerA implement Interface1{
		//静态内部类中的内部接口
		interface Interface2{
			//接口中的接口
			interface Interface3{}
		}

	}
	
	//静态内部类中的接口Interface2,Interface3可以通过导包的方式被成员内部类实现。
	public class InnerB implements Interface2,Interface3{

	}

}

我们可以看到,这里静态内部类可以实现顶级类的内部接口。而顶级类的成员内部类也可以实现静态内部类的内部接口。这一点可以证明静态内部类在一定程度上可以当成其外部类的同级类来对待。

外部类和其静态内部类和其内部类的类加载问题
public class OrderTest {

	static { System.out.println("OuterClass loading");}

	public OrderTest(){
		System.out.println("OuterClass constructer loading");
	}

	public static class StaticInnerClass{
		static { System.out.println("StaticInnerClass loading");}
		public StaticInnerClass(){
			System.out.println("StaticInnerClass constructer loading");
		}
	}

	public class InnerClass{

		public InnerClass(){
			System.out.println("InnerClass constructer loading");
		}
	}
}

以上是一个外围类OrderTest和其静态内部类StaticInnerClass和其内部了InnerClass

我们首先实例化外围类的内部类

public class test {

	static { System.out.println("test loading");}
	public static void main(String[] args) {
		OrderTest o = new OrderTest();
		OrderTest.InnerClass i = o.new InnerClass()
	}
}

内部类的实例化必须依靠外围类对象来实例化,所以结果是

test loading
OuterClass loading
OuterClass constructer loading
InnerClass constructer loading

首先外部类静态成员加载,外部类构造函数执行,才轮到内部类的构造函数执行(内部类不能有静态域)

再看外部类的加载

public class test {

	static { System.out.println("test loading");}
	public static void main(String[] args) {
		OrderTest o = new OrderTest();
	}
}


结果

test loading
OuterClass loading
OuterClass constructer loading

明显静态内部类虽然是外围类的静态域成员,但是不会加载。

而我们单独加载静态内部类

public class test {

	static { System.out.println("test loading");}
	public static void main(String[] args) {
		StaticInnerClass s = new StaticInnerClass();
	}
}

结果

test loading
StaticInnerClass loading
StaticInnerClass constructer loading

所以这样可以证明,静态内部类是相当于独立于外围类的一个类,仅仅是定义在外围类内部。而普通内部类是附属于外部类的一个成员。



参考资料

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值