内部类的概念和定义语法

面向对象类是最小的程序单元,但在某些情况下,也会把一个类放在另一个类的内部来定义,这个定义在其他类内部的类就被称为内部类也叫嵌套类,包含内部类的类称为外部类也叫宿主类,主要作用如下:

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类
  • 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类的成员,同一个类的成员之间可以互相访问,但外部类不能访问内部类的实现细节,例如内部类的成员变量
  • 匿名内部类适合用于创建哪些仅需要一次使用的类
  1. 定义内部类与定义外部类的语法大致相同,除了内部类需要定义在其他类里面之外,内部类比外部类可以多使用3个修饰符:private、protected、static,外部类不可以使用这三个修饰符
  2. 非静态内部类不能拥有静态成员

非静态内部类和静态内部类

内部类定义很简单,只需要将类放在另一个类内部定义即可,此处的内部类包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类称为局部内部类)

public class OuterClass
{
控制符 calss innerClass
}

通常内部类被作为成员内部类定义,而不是作为局部内部类,成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员,局部内部类和匿名内部类则不是类成员。
成员内部分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类非静态内部类

  • 外部类的上一级程序单元是package,所以它有两个作用域,同一个包内和任何位置,即package访问权限和公开访问权限,正好对应default访问控制符和public访问控制符
  • 内部类的上一级程序单元室外部类,它具有4个作用域:同一个类、同一个包、父子类和任何位置,因此可以使用4中访问控制权限
public class Cow
{
private double weight;
// 外部类的两个重载的构造器
public Cow(){}
public Cow(double weight)
{
this.weight = weight;
}
// 定义一个非静态内部类
private class CowLeg
{
// 非静态内部类的两个实例变量
private double length;
private String color;
// 非静态内部类的两个重载的构造器
public CowLeg(){}
public CowLeg(double length, String color)
{
this.length = length;
this.color = color;
}
public void setLength(double length)
{
this.length = length;
}
public double getLength()
{
return this.length;
}
public void setColor(String color)
{
this.color = color;
}
public String getColor()
{
return this.color;
}
// 非静态内部类的实例方法
public void info()
{
System.out.println("当前牛腿颜色是:" + color + ", 高:" + length);
// 直接访问外部类的private修饰的成员变量
System.out.println("本牛腿所在奶牛重:" + weight); // ①
}
}
public void test()
{
var cl = new CowLeg(1.12, "黑白相间");
cl.info();
}
public static void main(String[] args)
{
var cow = new Cow(378.9);
cow.test();
}
}

编译的时候会生成两个class文件,一个是Cow.class,另一个是​​Cow$CowLeg.class​​​,前者是外部类Cow的class文件,后者是内部类CowLeg的class文件,即成员内部类(包括静态内部类和非静态内部类)的class文件总是这种形式:​​OuterClass$InnerClass.class​​​ 在非静态内部类里可以直接访问外部类的private成员,​​System.out.println("本牛腿所在奶牛重:" + weight);​​这行代码就是在CowLeg类的方法内直接访问其外部类的private实例变量

因为在非静态内部类对象里,保存了一个它所寄生的外部类对象的应用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在外部类实例里)

Java面向对象系列[v1.0.0][内部类]_经验分享


当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量,如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量,如果不存在,则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量,如果依然不存在,系统报异常。

因此如果外部类成员变量,内部类成员变量与内部类里方法的局部变量同名,则可通过使用this、外部类类名.this作为限定来区分

public class DiscernVariable
{
private String prop = "外部类的实例变量";
private class InClass
{
private String prop = "内部类的实例变量";
public void info()
{
var prop = "局部变量";
// 通过 外部类类名.this.varName 访问外部类实例变量
System.out.println("外部类的实例变量值:" + DiscernVariable.this.prop);
// 通过 this.varName 访问内部类实例的变量
System.out.println("内部类的实例变量值:" + this.prop);
// 直接访问局部变量
System.out.println("局部变量的值:" + prop);
}
}
public void test()
{
var in = new InClass();
in.info();
}
public static void main(String[] args)
{
new DiscernVariable().test();
}
}

非静态内部类的成员可以访问外部类的private成员,但反过来就不行,非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用
如果外部类需要访问非静态内部类的成员,则必须显示创建非静态内部类对象来调用访问其实例成员

public 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);
// 如需访问内部类的实例变量,必须显式创建内部类对象
System.out.println("内部类的inProp值:" + new Inner().inProp);
}
public static void main(String[] args)
{
// 执行下面代码,只创建了外部类对象,还未创建内部类对象
var out = new Outer();
out.accessInnerProp();
}
}

mian方法中创建了一个外部类对象,并调用外部类对象的accessInnerProp()方法,此时非静态内部类对象根本不存在,如果允许accessInnerProp()方法访问非静态内部类对象,则必然报错

非静态内部类对象和外部类对象的关系

  • 非静态内部类对象必须寄生在外部类对象里,而外部类对象不一定有非静态内部类对象寄生其中,换句话说如果存在一个非静态内部类对象,则一定存在一个被他寄生的外部类对象,外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。
  • 外部类对象访问非静态内部类成员时,可能非静态普通内部类对象gen’be根本不存在
  • 非静态内部类对象访问外部类成员时,外部类对象一定存在
    同时根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等,总之不允许外部类的静态成员直接使用非静态内部类
public class StaticTest
{
// 定义一个非静态的内部类,是一个空类
private class In{}
// 外部类的静态方法
public static void main(String[] args)
{
// 下面代码引发编译异常,因为静态成员(main()方法)
// 无法访问非静态成员(In类)
new In();
}
}

Java不允许在非静态内部类里定义静态成员(静态方法、静态成员变量、静态初始化块等)

public class InnerNoStatic
{
private class InnerClass
{
/*
下面三个静态声明都将引发如下编译错误:
非静态内部类不能有静态声明
*/
static
{
System.out.println("==========");
}
private static int inProp;
private static void test(){}
}
}

非静态内部类不可以有静态初始化块,但可以有普通初始化块

静态内部类

使用static修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象,使用static修饰内部类也可称为类内部类
static的作用是把类成员变成类相关,而不是实例相关,就是说static修饰的成员属于类不属于单个对象
外部类的上一级程序单元是package,所以不可以使用static修饰,内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关,即static不可修饰外部类但可修饰内部类
静态内部类可以包含静态成员,也可以包含非静态成员,根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员,即便是静态内部类的实例方法也不能访问外部类的实例成员只能访问外部类的静态成员

public class StaticInnerClassTest
{
private int prop1 = 5;
private static int prop2 = 9;
static class StaticInnerClass
{
// 静态内部类里可以包含静态成员
private static int age;
public void accessOuterProp()
{
// 下面代码出现错误:
// 静态内部类无法访问外部类的实例变量
System.out.println(prop1);
// 下面代码正常
System.out.println(prop2);
}
}
}

静态内部类是外部类的类相关的,而不是外部类的对象相关的,静态内部类对象不是寄生在外部类的实例中,而是寄生在外部类的类本身中。当静态内部类对象存在时,并不存在一个被他寄生的外部类对象,静态内部类对象只持有外部类的类引用,没有外部类对象的引用,如果允许静态内部类的实例方法访问外部类的实例成员,但找不到被寄生的外部类对象,则系统会爆异常
静态内部类是外部类的一个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等
外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员

public class AccessStaticInnerClass
{
static class StaticInnerClass
{
private static int prop1 = 5;
private int prop2 = 9;
}
public void accessInnerProp()
{
// System.out.println(prop1);
// 上面代码出现错误,应改为如下形式:
// 通过类名访问静态内部类的类成员
System.out.println(StaticInnerClass.prop1);
// System.out.println(prop2);
// 上面代码出现错误,应改为如下形式:
// 通过实例访问静态内部类的实例成员
System.out.println(new StaticInnerClass().prop2);
}
}

内部接口及接口里的内部类

Java允许在接口里定义内部类,接口定义的内部类默认public static修饰,也就是说只能是静态内部类,且接口内部类的访问控制符必须是public,如果省略则系统默认是public访问权限。
接口里的内部接口是接口的成员,系统默认public static修饰,然而接口的目的是定义规范暴露给外界,因此定义接口里的内部接口没什么意义,但语法上是允许的。

在外部类内部使用内部类

  • 在外部类内部使用内部类,与平常使用普通类没有太大的区别,一样可以直接通过内部类类名来定义变量,通过new调用内部类构造器创建实例
  • 唯一存在的区别是:不要在外部类的静态成员(包括静态方法,静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员
  • 外部类中定义内部类的子类与平常定义子类也无大差别

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

如果希望在外部类以外的地方访问内部类(包括静态和非静态),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。
对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用:

  • 省略访问控制符,只能被与外部类处于同一个package中的其他类所访问
  • protected修饰的内部类,可以被与外部类处于同一个package中的其他类和外部类的子类所访问
  • public修饰的内部类,可以在任何地方被访问
    在外部类以外的地方定义内部类的(静态和非静态)变量,语法如下:
    ​​​OuterClass.InnerClass varName​​​ 可以看出的是,在外部类以外的地方使用内部类的时候,内部类的类名应该是OuterClass.InnerClass,如果外部类还有package名,则还应该增加package名前缀
    由于非静态内部类对象必须寄生在外部类的对象里,因此创建非静态内部类对象之前,必须先创建外部类对象,在外部类以外的地方创建非静态内部类实例,语法如下:
    ​OuterInstance.new InnerConstructor()​​ 在外部类以外的地方创建非静态内部类对象必须使用外部类实例和new来调用非静态内部类的构造器。
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("测试信息");
/*
上面代码可改为如下三行代码:
使用OuterClass.InnerClass的形式定义内部类变量
Out.In in;
创建外部类实例,非静态内部类实例将寄存在该实例中
Out out = new Out();
通过外部类实例和new来调用内部类构造器创建非静态内部类实例
in = out.new In("测试信息");
*/
}
}

当创建一个子类的时候,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时则必须存一个外部类对象

public class SubClass extends Out.In
{
//显示定义SubClass的构造器
public SubClass(Out out)
{
//通过传入的Out对象显示调用In的构造器,super代表调用In类的构造器
out.super("hello");
}
}

如果需要创建SubClass对象时,必须先创建一个Out对象,因为SubClass是非静态内部类In类的子类,非静态内部类In对象里必须有一个对Out对象的引用,其子类SubClass对象里也应该持有对Out对象的引用,当创建SubClass对象时传给该构造器的Out对象就是SubClass对象里Out对象引用所指向的对象
非静态内部类In对象和SubClass对象都必须持有指向Out对象的引用,区别是创建两种对象时传入Out对象方式不同,创建In的对象时必须通过Out对象来调用new关键字;创建SubClass类的对象时,必须使用Out对象作为调用者来调用In类的构造器。

非静态内部类的子类不一定是内部类,可以是一个外部类,但非静态内部类的子类实例一样需要保留一个引用,该引用指向其父类所在外部类的对象,也就是说如果有一个内部类子类的对象存在,则一定存在与之对应的外部类对象。

在外部类意外使用静态内部类

因为静态内部类是外部类类相关的,因此创建静态内部类对象时无需创建外部类对象,在外部类以外的地方创建静态内部类实例

new OuterClass.InnerConstructor()
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();
/*
上面代码可改为如下两行代码:
使用OuterClass.InnerClass的形式定义内部类变量
StaticOut.StaticIn in;
通过new来调用内部类构造器创建静态内部类实例
in = new StaticOut.StaticIn();
*/
}
}

无论是静态内部类还是非静态内部类,他们声明变量的语法完全一样,区别只是在创建内部类对象时,静态内部类只需要使用外部类即可调用构造器,非静态内部类必须使用外部类对象来调用构造器

创建静态内部类子类语法

​public class StaticSubClass extends StaticOut.StaticIn {}​​ 子类中的内部类和父类中的内部类不可能完全重名,因为他们的名字都带上了外部类名,也就不可能重写父类的内部类

匿名内部类

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

new 实现接口() | 父类构造器(实参列表)
{
// 匿名内部类的类体
}
  • 匿名内部类必须继承一个父类,或者实现一个接口,但最多只能继承一个父类或实现一个 接口
  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象,因此不允许匿名内部类定义为抽象类
  • 匿名内部类不能定义构造器,它没有类名无法定义构造器,但可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事
interface Product
{
double getPrice();
String getName();
}
public class AnonymousTest
{
public void test(Product p)
{
System.out.println("His name is" + p.getName() + ",His price is" + 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 "davieyang";
}
});
}
}

test方法需要个Product对象作为参数,而Product是个接口,无法直接创建对象,因此需要实现Product接口,如果实现这个接口的类需要重复使用,则应该定义一个独立的类,如果只是一次性使用则可以用匿名内部类
匿名内部类不能是抽象类,因此它必须实现它的抽象父类或者接口里的所有抽象方法,上边的匿名内部类的还可以写成:

class AnonymousProduct implements Product
{
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "davieyang"
}
}
ta.test(new AnonymousProduct());

显然匿名构造类的写法更加简单

  • 当通过实现接口来创建匿名内部类时,匿名内部类也不能显示创建构造器,因此匿名内部类只有一个隐式的无参数构造器,因此new接口名后的括号里不能传入参数值
  • 如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器(拥有相同的形参列表)
abstract class Device
{
private String name;
public 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);
}
}

创建匿名内部类时,必须实现接口或抽象父类中的所有抽象方法,如果有必要还可以重写父类中的普通方法
在Java8之前,要求被局部内部类、匿名内部类访问的局部变量必须使用final修饰,Java8之后无此限制而是如果局部变量贝宁名内部类访问,那么局部变量相当于自动使用了final修饰

interface A
{
void test();
}
public class ATest
{
public static void main(String[] args)
{
int age = 8; // ①
// 下面代码将会导致编译错误
// 由于age局部变量被匿名内部类访问了,因此age相当于被final修饰了
// age = 2;
var a = new A()
{
public void test()
{
// 在Java 8以前下面语句将提示错误:age必须使用final修饰
// 从Java 8开始,匿名内部类、局部内部类允许访问非final的局部变量
System.out.println(age);
}
};
a.test();
}
}

​effectively final​​​,其意思是对于被匿名内部类访问的局部变量,可以用final修饰,也可以不用final修饰,但必须按照有final修饰的方式来用,即一次赋值后不能再次赋值。
局部内部类
如果把一个内部类放在一个方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效,且不能使用访问控制符和static修饰,因为他们的上一级程序单元是方法,而不是类因此不管是局部变量还是局部内部类使用static都毫无意义,所有局部成员都不能使用static,并且局部成员的作用域是所在方法,其他程序单元永远无法访问到,因此使用访问控制符也无意义

如果需要用局部内部类定义变量、创建实例或派生子类都只能在局部内部类所在的方法内进行

public class LocalInnerClass
{
public static void main(String[] args)
{
// 定义局部内部类
class InnerBase
{
int a;
}
// 定义局部内部类的子类
class InnerSub extends InnerBase
{
int b;
}
// 创建局部内部类的对象
var is = new InnerSub();
is.a = 5;
is.b = 8;
System.out.println("InnerSub对象的a和b实例变量是:" + is.a + "," + is.b);
}
}

编译这个文件会生成3个class文件,​​LocalInnerClass.class/LocalInnerClass$1InnerBase.class/LocalInnerClass$1InnerSub.class​​​ 他们遵循的规则是OuterClass$NInnerClass.class局部内部类的class文件名比成员内部类的class文件名多了个数字,这是因为同一个类里不可能有两个同名的成员内部类,同一个类里则可能有两个以上同名的局部内部类分散在不同的方法中而已,所以增加个数字用于区分
​局部内部类是个鸡肋,创建类是为了复用,而局部内部类太扯​