首页 > 技术文章 > 5.7 内部类

weststar 2020-03-07 23:11 原文

一、内部类的定义

在某些情况下,我们将一个类放在另一个类的内部定义,这个定义在其他类内部的类就叫做内部类(嵌套类),包含内部类的外部类也被称为外部类(宿主类)。
内部类的主要作用:
1、内部类提供更好的封装,可以把内部类隐藏在外部类之中,不允许同一个包中的其他类访问该类。
2、内部类成员可以访问外部类的私有数据,因为内部类被当成外部类的成员,同一个类中的成员之间可以相互访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
3、匿名内部类适合用于创建那些只需要使用一次的类
内部类组要注意的两点:
1、内部类的修饰符比外部类多三个:protected public static.
2、非静态内部类不能用于静态成员变量。

二、非静态内部类

2.1 非静态内部类的定义

内部类一定放在一个类的类体部分(花括号内)定义,内部类的定义语法:

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

大部分的时候,内部类作为成员内部类被定义,而不是局部内部类。成员内部类是一种与成员变量、方法、构造器、初始化块相似的类成员,所以可以使用任意访问控制符;局部内部类和匿名内部类不是类成员。
注意:外部类的上一次程序单元是包,所以他只有两个作用域:同一个包和任何位置。对应两种控制符不写、public
内部类上一级程序单元式外部类,因此具有四个作用域:同一个类、同一个包中、父子类和任何位置。因此可以有四种访问权限。
下面在Cow类里定义一个CowLeg非静态内部类,并在CoeLeg类的实例中直接访问Cow类的private访问权限的实例变量。

public class Cow
{
	private double weight;
	//外部类的两个构造器
	public Cow(){}
	public Cow(double weight)
	{
		this.weight=weight;
	}

	//定义一个非静态内部类
	public class CowLeg
	{
		//非静态内部类的两个实例变量
		private double length;
		private String color;
		public CowLeg(){}
		public CowLeg(double length,String color)
		{
			this.length=length;
			this.color=color;
		}

		//非静态内部类的实例方法
		**public void info()
		{
			System.out.println("当前牛腿颜色是:"+color+",长:"+length);
			//直接访问外部类的private修饰的成员变量
			System.out.println("本腿所在奶牛重:"+weight);//①
		}**
	}
	public void test()
	{
		var c1=new CowLeg(1.2,"黑白相间");
		c1.info();
	}
	public static void main(String[] args)
	{
		var cow=new Cow(378.9);
		cow.test();
	}
}
---------- 运行Java捕获输出窗 ----------
当前牛腿颜色是:黑白相间,长:1.2
本腿所在奶牛重:378.9

输出完成 (耗时 0 秒) - 正常终止

编译上面的程序,将看到在文件所在路径生成两个class文件,一个是Cow.class,另一个是 Cow$CowLeg.class,前者是一个外部类,后者是内部类CowLeg的class文件,即成员变量(包括静态内部类和非静态内部类)的class文件总是以这样的形式:OutClass$InnerClass.class

上面①号粗体部分就是在内部类CowLeg中访问外部类private实例成员。因为这是在非静态内部类中,保存了它所寄生的外部类的应用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在外部类实例中)。下图显示上面程序在运行时在内存中的示意图:

2.2 非静态内部类的成员访问外部类的实例成员

当非静态内部成员的方法访问某个变量时,系统查找顺序:方法体存在的局部变量————>该变量所在方法体的内部内中查找————>外部类。若都不存在则出现编译错误:提示找不到该变量。如果外部类、内部类、局部成员变量名相同,则可以通过this,外部类名.this作为区分。如下程序所示:

public class DiscernVariable
{
	private String prop="外部类实例成员";
	private class InClass
	{
		private String prop="内部类实例成员变量";
		public void info()
		{
			var prop="方法体内局部变量";
			System.out.println("外部类实例成员变量值:"+DiscernVariable.this.prop);
			System.out.println("内部类类实例成员变量值:"+this.prop);
			System.out.println("局部变量值:"+prop);
		}
	}
	public void test()
	{
		var in=new InClass();
		in.info();
	}
	public void test1()
	{
		System.out.println(prop);
	}
	public static void main(String[] args)
	{
		DiscernVariable d=new DiscernVariable();
		d.test();
		d.test1();
	}
}
---------- 运行Java捕获输出窗 ----------
外部类实例成员变量值:外部类实例成员
内部类类实例成员变量值:内部类实例成员变量
局部变量值:方法体内局部变量
外部类实例成员
输出完成 (耗时 0 秒) - 正常终止

2.3 外部类不能直接访问内部类实例成员

如果需要在外部类访问内部类实例成员,则必须显示创建非静态内部类对象来调用访问其实例成员。

class Outer 
{
	private int outProp=9;
	class Inner
	{
		private int inProp=5;
		public void accessOuterProp()
		{
			//非静态内部类直接访问外部类的private实例成员
			System.out.println("外部类的outProp值:"+outProp);
		}
	}
	public void accessInnerProp()
	{
		//外部类不能直接访问非静态内部类实例成员变量
		//System.out.println("内部类的inProp值:"+inProp);
		//Outer.java:16: 错误: 找不到符号
		//外部类访问内部类的实例成员变量必须显示创建内部类对象
		System.out.println("内部类的inProp值:"+new Inner().inProp);
	}
	public static void main(String[] args)
	{
		var out=new Outer();
		out.accessInnerProp();
	}
}
---------- 运行Java捕获输出窗 ----------
内部类的inProp值:5

输出完成 (耗时 0 秒) - 正常终止

2.4不允许在外部静态成员中访问内部类非静态成员

静态成员不能访问非静态成员,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。

class StaticTest 
{
	//定义一个非静态内部类:是一个空类
	private class In
	{
	}
	//外部静态方法
	public static void main(String[] args)
	{
		new In();
	}
}
StaticTest.java:10: 错误: 无法从静态上下文中引用非静态 变量 this

2.5 Java不允许在非静态内部类定义非静态成员

class  InnerNoStatic
{
	private class InnerClass
	{
		static int num=5;
	}
}
---------- 编译Java ----------
InnerNoStatic.java:5: 错误: 内部类InnerNoStatic.InnerClass中的静态声明非法
		static int num=5;
		           ^
  修饰符 'static' 仅允许在常量变量声明中使用
1 个错误

输出完成 (耗时 1 秒) - 正常终止

非静态内部类不能有静态成员,包括静态方法、静态初始化块、静态成员变量。

三、静态内部类

如果使用static修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类称为类内部类,有的地方也称为静态内部类。

3.1 静态内部类不能访问外部类实例成员

注:静态内部类可以包含静态成员变量,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。

public class StaticInnerClassTest  
{
	//外部类的实例成员
	private int prop1=5;
	//外部类的类成员变量
	private static int prop2=9;

	//定义静态内部类
	static class StaticInnerClass
	{
		//静态内部类可以包含静态成员和非静态成员
		private static int age;
		private String name;
		public void accessOuterProp()
		{
			//静态内部类无法访问外部类的实例变量
			//System.out.println(prop1);//错误: 无法从静态上下文中引用非静态 变量 prop1
			System.out.println(prop2);
		}
	}
}

</font color=red>静态内部类是外部类相关的,而不是外部类对象相关的。也就是说,静态内部类对象不是寄生在外部类的实例中。当静态内部类对象存在时,并不存在一个被它寄生的外部类对象,静态内部类对象只持有外部类的引用,没有持有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员,但找不到外部类对象的引用,这将引起编译错误。

3.2 外部类访问静态内部类成员

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

public class AccessStaticInnerClass 
{
	//定义静态内部类
	static class StaticInnerClass
	{
		private static String prop1="静态内部类类成员";
		private String prop2="静态内部类实例成员";
	}
	public void accessInnerProp()
	{
		//通过类名访问静态成员
		//System.out.println(prop1);//错误: 找不到符号
		System.out.println(StaticInnerClass.prop1);
		//通过对象来访问外部类的实例成员
		System.out.println(new StaticInnerClass().prop2);
	}
	public static void main(String[] args)
	{
		var a=new AccessStaticInnerClass();
		a.accessInnerProp();
	}
}
---------- 运行Java捕获输出窗 ----------
静态内部类类成员
静态内部类实例成员

输出完成 (耗时 0 秒) - 正常终止

3.3 接口中内部类

Java允许在接口中定义内部类,接口中定义的内部类默认使用public static修饰符,也就是说接口内部类只能是静态内部类。

非静态内部类和静态内部类小结:

非静态内部类

静态内部类:

四、使用内部类

4.1 在外部类内部使用内部类

在外部类使用内部类时,可以直接通过内部类类名来定义变量,通过new关键字调用内部类构造器创建实例。
唯一区别:不要在外部类的静态成员中(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。

4.2在外部类以外使用非静态内部类

如果希望在外部类以外的地方访问内部类(包括静态和非静态),则内部类不能使用private访问控制权限。private修饰的内部类只能在外部类内部使用。对于其他访问控制符修饰的内部类,则可以在访问控制符对应的访问权限内使用。
1、省略访问控制符的内部类,只能与外部类处于同一个包中的其他类所访问。
2、使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。
3、public修饰的内部类,可在任何地方被访问。
在外部类以外的其他地方定义内部类(包括静态和非静态两种)变量的语法格式:
OuterClass.InnerClass varName
在外部类以外的其他地方定义内部类时,内部类完整的类名应该是OuterClass.InnerClass。如果外部类有包名,则还应该增加包名前缀。
在外部类以外的地方创建非静态内部类实例的语法格式:
outerInstance.new InnerConstructor()
下面程序示范在外部类以外的地方创建非静态内部类实例的对象,并把它赋给非静态内部类类型的变量。

class Out 
{
	//定义一个内部类,不使用修饰符
	//可以在同一个包中的其他类访问
	class In
	{
		public In(String msg)
		{System.out.println(msg);}
	}
}
public class CreateInnerInstance
{
	public static void main(String[] args)
	{
		Out.In in=new Out().new In("测试信息");
		//上面一行代码等价与下面三行代码
		Out.In in1;//使用OuterClass.InnerClass形式定义内部类变量
		Out out=new Out();//创建外部类实例,因为非静态内部类属于外部类对象
		in1=out.new In("分布讲解");//通过外部类实例调用内部类构造器创建非静态内部类
		}
}
---------- 运行Java捕获输出窗 ----------
测试信息
分布讲解

输出完成 (耗时 0 秒) - 正常终止

如果需要在外部类以外的地方创建非静态内部类的子类,需要注意以下规则:非静态内部类的构造器必须通过外部类的对象来调用。
当创建一个子类时,子类构造器会调用父类构造器,因此创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类的对象。

class SubClass extends Out.In 
{
	public SubClass(Out out)
	{
		//通过out对象显示调用In构造器
		out.super("hello");
	}
	public static void main(String[] args) 
	{
		Out out=new Out();
		var sub=new SubClass(out);
	}
}

如果需要创建SubClass对象时,必须先创建一个Out对象。因为SubClass是非静态内部类In类的子类,非静态内部类In对象必须有一个Out对象的引用,其子类SubClass对象也应该持有堆out对象的引用。当创建SubClass对象时,传给该构造器的out对象,就是SubClass对象引用所指向的对象。

4.3 在外部类以外使用静态类

因为静态类时外部类相关的,因此创建静态类无需创建外部类对象。在外部类以外的地方创建静态内部类实例的语法:
new outerClass.InnerComsatructor()
下面程序示例如何在外部类以外的其他地方发创建静态内部类的实例:

class StaticOut 
{
	//定义一个静态内部类,不使用访问控制符
	//可以在同一个包中的其他类或该类中访问内部类
	static class StaticIn
	{
		public StaticIn()
		{
			System.out.println("静态内部类构造器");
		}
	}
}

public class CreateStaticInnerInstance
{
	public static void main(String[] args)
	{
		StaticOut.StaticIn in=new StaticOut.StaticIn();
	}
}
---------- 运行Java捕获输出窗 ----------
静态内部类构造器

输出完成 (耗时 0 秒) - 正常终止

因为调用静态内部类构造器无需使用外部类对象,所以创建内部类子类的方法也比较简单:
public class StaticSubClass extends StaticOut.StaticIn { //类体 }
相比之下,使用静态内部类比使用非静态内部类简单,只需要把外部类当成静态内部类的包空间即可。因此程序中需要使用内部类时,优先考虑使用静态内部类

五、局部内部类

把一个类放到方法里定义,这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能再外部类的方法以外引用,因此局部内部类也不能使用访问控制符和static修饰符修饰。
如果需要用局部内部类定义变量、创建实例或派生子类,那么局部内部类只能在局部内部类所在的方法里进行。

class LocalInnerClass 
{
	public static void main(String[] args) 
	{
		//定义局部内部类
		class InnerBase
		{
			int a=5;
		}
		//定义局部内部类的子类
		class SubInner extends InnerBase
		{
			int b=8;
		}
		//创建局部内部类对象
		var in =new SubInner();
		System.out.println(in.a);
		System.out.println(in.b);
	}
}
---------- 运行Java捕获输出窗 ----------
5
8

输出完成 (耗时 0 秒) - 正常终止

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

六、匿名内部类

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

new 实现接口() |父类构造器(实参列表)
{
//匿名内部类的类体部分
}

匿名内部类必须继承一个父类或实现一个接口,但最多只能继承一个父类或实现一个接口。关于匿名内部类的两条规则:
1、匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许把匿名内部类定义为抽象类。
2、匿名内部类不能定义构造器,由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器要完成的事。

6.1 通过实现接口创建匿名内部类

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

interface Product
{
	double getPrice();//抽象方法
	String getName();
} 
public class AnonymousTest
{
	//调用内部类
	public void test(Product p)
	{
		System.out.println("购买一个"+p.getName()+",花掉了"+p.getPrice());
	}
	public static void main(String[] args)
	{
		var ta=new AnonymousTest();
		//调用test()方法时,需要传入一个人Product的数据
		//此处传入的是匿名实现类的实例
		ta.test(new Product()
		{
			public double getPrice()
			{
				return 567.8;
			}
			public String getName()
			{
				return "AGP显卡";
			}
		});
	}
}
---------- 运行Java捕获输出窗 ----------
购买一个AGP显卡,花掉了567.8

输出完成 (耗时 0 秒) - 正常终止

上面程序在AnonymousTest类定义了一个test()方法,该方法需要一个Product对象的参数,但Product是一个接口,无法直接创建对象,因此考虑创建一个接口实现类对象闯入该方法。
当通过接口来创建匿名内部类时,匿名内部类不能显式地定义构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值。

6.2 通过继承父类来创建匿名内部类

通过继承父类来创建匿名内部类时,匿名内部类将拥有,将拥有和父类相似的构造器,此处相似指的是拥有和父类相似的形参列表。

abstract class Device
{
	private String name;
	abstract double getPrice();
	public Device(){}
	public Device(String name)
	{
		this.name=name;
	}
	public void setName(String name)
	{
		this.name=name;
	}
	public String getName()
	{
		return this.name;
	}
} 
public class AnonymousInner
{
	public void test(Device d)
	{
		System.out.println("购买一个"+d.getName()+",花掉了"+d.getPrice());
	}
	public static void main(String[] args)
	{
		var ai=new AnonymousInner();
		//调用有参数构造创建Device匿名实现类对象
		ai.test(new Device("电子示波器")
			{
				public double getPrice()
				{
					return 67.8;
				}
			});
			
		//调用无参数构造器创建Device匿名实现类
		var d=new  Device()
		{
			//初始化块
			{
				System.out.println("匿名内部类的初始化块...");
			}
			//实现抽象方法
			public double getPrice()
			{
				return 56.2;
			}
			//重写父类的实例方法
			public String getName()
			{
				return "键盘";
			}

		};
		ai.test(d);
	}
}
---------- 运行Java捕获输出窗 ----------
购买一个电子示波器,花掉了67.8
匿名内部类的初始化块...
购买一个键盘,花掉了56.2

输出完成 (耗时 0 秒) - 正常终止

当创建匿名内部类时,必须实现接口或抽象父类的所有抽象方法。如果有需要,也可以重写父类里的普通方法。

推荐阅读