【Java25】内部类

把一个类定义在另一个类内部,就是内部类(也叫嵌套类)。包含内部类的类称为外部类(也叫宿主类)。内部类的好处在于:

  1. 更好的封装。内部类隐藏在外部类之内,同一个包内的其他类就没法访问了;
  2. 内部类的成员可以直接访问外部类的private成员(变量、方法);外部类也可以直接访问内部类的private成员;
  3. 一些比较简单的类,可能只会被调用一次。这时不需要独立地定义这个类,而是把它们嵌入到外部类里。

大部分情况下,把内部类看成是成员内部类,也就是和成员变量、成员方法、构造器、初始化块类似的东西(想一想能够放在Java类内部的五种东西)。因此,成员内部类也就有两种:static修饰的“类内部类”(太拗口了,所以还是遵循传统叫静态内部类,或者就直接叫static修饰的成员内部类)和普通内部类。除此之外,成员内部类做为外部类的组成部分,具有访问权限,即public,private,protected

外部类的上一级程序单元是,所以它只有两个作用域:同一个包内和任意位置。因此,外部类只有两个访问权限:包访问权限,也就是default或者直接缺省;和公开访问权限,也就是public。内部类则具有完整的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;
    }
    // 省略get和set方法
    private void info()
    {
      System.out.println("奶牛的重量:"+weight); // 直接访问外部类的私有成员
    }
  }
  public void test()
  {
    var c1 = new CowLeg(1.12, "黑白相间");
    System.out.println(c1.length);
    c1.info(); // 直接访问内部类的私有成员变量和方法
  }
  public static void main(String args[])
  {
    var cow = new Cow(378.9);
    cow.test();
  }
}

上述代码中,外部类Cow嵌套了一个内部类CowLeg。在编译之后,会生成两个class文件:一个是Cow.class,另一个则是CowCowLeg.class。内部类编译后总是以‘外部类CowLeg.class。内部类编译后总是以`外部类CowLeg.class。内部类编译后总是以外部类内部类.class`形式存在。

内部类CowLeg之所以能够访问外部类的私有成员,是因为在非静态的内部类对象中,保存了一个所寄生的外部类对象的引用。

非静态内部类实例必须寄生在外部类实例中。

上述代码的内存分配情况如下图:

在这里插入图片描述

但是,反过来,外部类访问内部类就不成立了。外部类想要访问非静态的内部类,需要显式实例化内部类,如第34行。

一个非静态内部类对象的存在,肯定有一个被寄生的外部类对象。

但是一个外部类对象存在时,其非静态内部类对象并不一定存在!

此外,还要遵循静态成员不能访问非静态成员的规则,外部类的类方法、类代码块不能访问非静态内部类。

静态内部类(类内部类)

使用static修饰内部类以后,内部类就变成“类内部类”,说白了,就属于外部类本身,而不属于外部类的某个具体对象了。

同样,遵循静态成员不能访问非静态成员的规则,类内部类不能访问外部类的实例变量。

一个类内部类存在时,一定存在外部类本身。但是,外部类的实例并不一定存在。所以,在类内部类去试图访问外部类的实例变量或实例方法,就会引起错误。

反过来,外部类的所有方法、所有初始化块都可以使用类内部类。外部类在使用类内部类的类成员时,通过类内部类名.成员来调用;使用类内部类的实例成员时,通过实例化类内部类对象来调用:

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(new StaticInnerClass().prop2);
  }
}

Java还允许接口里定义内部类,默认使用public static修饰。

同样,Java还允许接口里定义接口,内部接口默认也是使用public static修饰的。

Java还允许类内定义接口。内部接口默认都是static的,后续的枚举也是,默认static。

在外部类之外使用内部类

private修饰的内部类只能在外部类中使用。若想在外部类之外使用非静态成员类,需要把内部类的访问权限设为:

  1. public,在任何地方访问;
  2. protected,外部类同一个包的其他类和外部类的子类访问;
  3. default,外部类同一个包的其他类。

由于非静态成员类依赖外部类对象,因此在外部类之外使用时,首先要实例化一个外部类,然后再实例化该非静态成员类,最后再进行调用:

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 in;
    // Out out = new Out();
    // in = out.new In("测试信息");
  }
}

在外部类之外使用类内部类相对简单一点,这时外部类相当于一个包名。实例化的时候,不需要依赖外部类实例:

class StaticOut
{
  static class StaticIn
  {
    public StaticIn()
    {
      System.out.println("静态内部类的构造器");
    }
  }
}

public class CreateStaticInnerInstance
{
  public static void main(String[] args)
  {
    var in = new StaticOut.Staticin();
  }
}
局部内部类

如果在方法中定义一个内部类,这个类就是局部内部类了。局部内部类只在这个方法内有效。

局部内部类只在方法内有效,所以使用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;
  }
}

编译上述文件,生成LocalInnerClass.class,LocalInnerClass$1InnerBase.classLocalInnerClass$1InnerSub.class三个文件。可以发现规律:局部内部类的class文件在内部类的基础上,加了一位数字。这个数字是用来区别同名局部内部类的。

成员内部类是不可能重名的。但是局部内部类,由于位于不同的方法中,是可能重名的。

局部内部类是非鸡肋,代码冗长,功能有限。不建议使用。

匿名类

为了改进局部内部类的冗长性,对那种创建之后只使用一次的类,用匿名类更好。匿名类的特点是定义的同时就实例化了,从作用上看类似于赋值语句,是一次性的。

匿名类的语法如下:

new 实现接口() | 父类构造器(实参列表)
{
  // 匿名类的类体
}
  • 匿名类必须继承一个接口,或者一个父类。但最多继承一个。

  • 由于匿名类在定义的时候就要实例化(new),所以不能是抽象类。

  • 匿名类没有类名,所以没有办法定义构造器,但是可以用实例初始化块完成一些构造器要做的事情。

先看一个继承了接口的匿名类,这也是最常见的匿名类使用场景:

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引用变量
    // 这时可以使用匿名类实现接口Product,同时实例化一个匿名类对象,传入test方法
    ta.test(new Product() // 这里对应匿名类的语法:new 实现接口()
            {							// 类体
              public double getPrice()
              {
                return 567.8;
              }
              public String getName()
              {
                return "AGP显卡";
              }
            }); 
  }
}

由于接口不能直接实例化,正常情况下,需要定义一个类来实现(implement)这个接口,然后实例化这个类,让接口引用变量指向这个对象。“古典写法”应该是这样的:

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

上述定义的AnonymousProduct类既可以是普通类,也可以是内部类,也可以是局部类。但无论如何,其代码要比使用匿名类复杂一些。

匿名类属于为了代码简洁而设计的“炫技式”写法,但是确实使得代码简洁美观了。

上述匿名类不能接受参数传入,本质原因是接口不接受参数。要想让匿名类接受参数,那就让它继承一个类。此时,匿名类因为能够执行父类的带参数的构造器,从而接受外部传参。但请注意,匿名类是没有自己的构造器的,因此参数传入匿名类后,需要在实例初始化块中进行处理。

下面看一个继承类的匿名类:

abstract class Device
{
  private String name;
  public abstract double getPrice();
  public Device();
  public Device(String name)
  {
    this.name = name;
  }
  // 省略name的set和get方法
}

public class AnonymousInner
{
  public void test(Device d)
  {
    System.out.println("购买了一个"+p.getName()
                      + ",花掉了"+p.getPrice());
  }
  public static void main(String[] args)
  {
    var ai = new AnonymousInner();
    ai.test(new Device("电子示波器") // 匿名类继承类,调用父类的带参数构造器
            {
              // getName是父类的方法
              // getPrice是重写父类的抽象方法
              public double getPrice()
              {
                return 67.8;
              }
            });
    var d = new Device() 	// 抽象类本来无法实例化
    { 										// 利用匿名类继承父类,并重写其中的抽象方法,从而可以实例化
      // 没有构造器,只有初始化块
      {
        System.out.println("匿名内部类的初始化块");
      }
      // 实现父类的抽象方法
      public double getPrice()
      {
        return 56.2;
      }
      // 可以重写父类的实例方法
      public String getName()
      {
        return "键盘";
      }
    }
    ai.test(d);
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值