java面向对象(内部类)

68 阅读15分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情

3 内部类

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
  • 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
  • 匿名内部类适合用于创建那些仅需要一次使用的类。对于前面介绍的命令模式,当需要传入一个Command对象时,重新专门定义PrintCommand和AddCommand两个实现类可能没有太大的意义,因为这两个实现类可能仅需要使用一次。在这种情况下,使用匿名内部类将更方便。

3.1 非静态内部类

定义内部类非常简单,只要把一个类放在另一个类内部定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类被称为局部内部类)。

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

注意: 外部类的上一级程序单元是,所以它只有2个作用域:同一个包内和任何位置。而内部类的上一级程序单元是外部类,它就具有4个作用域:同一个类、同一个包、父子类和任何位置,因此可以使用4种访问控制权限。

下面程序在Cow类里定义了一个CowLeg非静态内部类,并在CowLeg类的实例方法中直接访问Cow的private访问权限的实例Field。

        public class Cow
        {
            private double weight;
            //外部类的两个重载的构造器
            public Cow(){}
            public Cow(double weight)
            {
                  this.weight=weight;
            }
            //定义一个非静态内部类
            private class CowLeg
            {
                  //非静态内部类的两个Field
                  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修饰的Field
                        System.out.println("本牛腿所在奶牛重:" + weight);   //①
                  }
            }
            public void test()
            {
                  CowLeg cl=new CowLeg(1.12 , "黑白相间");
                  cl.info();
            }
            public static void main(String[] args)
            {
                  Cow cow=new Cow(378.9);
                  cow.test();
            }
        }

如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this外部类类名.this作为限定来区分。 在这里插入图片描述

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

        public class Outer
        {
            private int outProp=9;
            class Inner
            {
                  private int inProp=5;
                  public void acessOuterProp()
                  {
                        //非静态内部类可以直接访问外部类的成员
                        System.out.println("外部类的outProp值:"
                            + outProp);
                  }
            }
            public void accessInnerProp()
            {
                  //外部类不能直接访问非静态内部类的实例Field
                  //下面代码出现编译错误
                  //System.out.println("内部类的inProp值:" + inProp);
                  //如需访问内部类的实例Field,则必须显式创建内部类对象
                  System.out.println("内部类的inProp值:"
                        + new Inner().inProp);
            }
            public static void main(String[] args)
            {
                  //执行下面代码,只创建了外部类对象,还未创建内部类对象
                  Outer out=new Outer();      //①
                  out.accessInnerProp();
            }
        }

非静态内部类对象和外部类对象的关系是怎样的? 答:非静态内部类对象必须寄存在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄存其中。简单地说,如果存在一个非静态内部类对象,则一定存在一个被它寄存的外部类对象。但外部类对象存在时,外部类对象里不一定寄存了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在!而非静态内部类对象访问外部类成员时,外部类对象一定存在。

根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。如下程序所示。

        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(){}
            }
        }

注意: 非静态内部类里不可以有静态初始化块,但可以包含普通初始化块。非静态内部类普通初始化块的作用与外部类初始化块的作用完全相同。

3.2 静态内部类

如果使用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修饰,也就是说,接口内部类只能是静态内部类。

3.3 使用内部类

(1)在外部类内部使用内部类 (2)在外部类以外使用非静态内部类 在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:

        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("测试信息");
              /*
              上面代码可改为如下三行代码:
              使用OutterClass.InnerClass的形式定义内部类变量
              Out.In in;
              创建外部类实例,非静态内部类实例将寄存在该实例中
              Out out=new Out();
              通过外部类实例和new来调用内部类构造器创建非静态内部类实例
              in=out.new In("测试信息");
              */
        }
    }

如果需要在外部类以外的地方创建非静态内部类的子类,则尤其要注意上面的规则:非静态内部类的构造器必须通过其外部类对象来调用。

我们知道:当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象。下面程序定义了一个子类继承了Out类的非静态内部类In类。

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

非静态内部类In对象和SubClass对象都必须保留有指向Outer对象的引用,区别是创建两种对象时传入Out对象的方式不同:当创建非静态内部类In类的对象时,必须通过Outer对象来调用new关键字;当创建SubClass类的对象时,必须使用Outer对象作为调用者来调用In类的构造器。

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

(3)在外部类以外使用静态内部类 因为静态内部类是外部类类相关的,因此创建内部类对象时无须创建外部类对象。

        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 {}

3.4 局部内部类

如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。

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

编译上面程序,看到生成了三个class文件:LocalInnerClass.class、LocalInnerClass1InnerBase.classLocalInnerClass1InnerBase.class和LocalInnerClass1InnerSub.class,这表明局部内部类的class文件总是遵循如下命名格式:OuterClass$NInnerClass.class。

3.5 匿名内部类

匿名内部类适合创建那种只需要一次使用的类,例如前面介绍命令模式时所需要的Command对象。匿名内部类的语法有点奇怪,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用

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

从上面定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。

  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
  • 匿名内部类不能定义构造器,因为匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义实例初始化块,通过实例初始化块来完成构造器需要完成的事情。

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

        interface Product
        {
            public double getPrice();
            public String getName();
        }
        public class AnonymousTest
        {
            public void test(Product p)
            {
                  System.out.println("购买了一个" + p.getName()
                        + ",花掉了" + p.getPrice());
            }
            public static void main(String[] args)
            {
                  AnonymousTest ta=new AnonymousTest();
                  //调用test方法时,需要传入一个Product参数
                  //此处传入其匿名实现类的实例
                  ta.test(new Product()
                  {
                        public double getPrice()
                        {
                            return 567.8;
                        }
                        public String getName()
                        {
                            return "AGP显卡";
                        }
                  });
            }
        }

上面程序中的AnonymousTest类定义了一个test方法,该方法需要一个Product对象作为参数,但Product只是一个接口,无法直接创建对象,因此此处考虑创建一个Product接口实现类的对象传入该方法——如果这个Product接口实现类需要重复使用,则应该将该实现类定义成一个独立类;如果这个Product接口实现类只需一次使用,则可采用上面程序中的方式,定义一个匿名内部类。

由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或者接口里包含的所有抽象方法。

  • 当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值。
  • 但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此处的相似指的是拥有相同的形参列表。
                              程序清单:codes\06\6.7\AnonymousInner.java
      abstract class Device
      {
          private String name;
          public abstract double getPrice();
          public Device(){}
          public Device(String name)
          {
                this.name=name;
          }
          //此处省略了name的setter和getter方法
          ...
      }
      public class AnonymousInner
      {
          public void test(Device d)
          {
          System.out.println("购买了一个" + d.getName()
                + ",花掉了" + d.getPrice());
          }
          public static void main(String[] args)
          {
                AnonymousInner ai=new AnonymousInner();
                //调用有参数的构造器创建Device匿名实现类的对象
                ai.test(new Device("电子示波器")
                {
                      public double getPrice()
                      {
                          return 67.8;
                      }
                });
                //调用无参数的构造器创建Device匿名实现类的对象
                Devicelt@span b=1> dlt@span b=1>=lt@span b=1> new Device()
                {
                      //初始化块
                      {
                          System.out.println("匿名内部类的初始化块...");
                      }
                      //实现抽象方法
                      public double getPrice()
                      {
                          return 56.2;
                      }
                      //重写父类的实例方法
                      public String getName()
                      {
                          return "键盘";
                      }
                };
                ai.test(d);
          }
      }

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

如果匿名内部类需要访问外部类的局部变量,则必须使用final修饰符来修饰外部类的局部变量,否则系统将报错。

        interface A
        {
            void test();
        }
        public class ATest
        {
            public static void main(String[] args)
            {
                  int age=0;
                  A a=new A()
                  {
                        public void test()
                        {
                            //下面语句将提示错误:
                            //匿名内部类内访问局部变量必须使用final修饰
                            System.out.println(age);
                        }
                  };
            }
        }

参考文献:《疯狂java讲义》李刚