把Java多学一点(1)------内部类详解

313 阅读6分钟

可以将一个类的定义放在另一个类的定义内部,这就是内部类。

首先我们先来,了解一下,java中为什么要使用内部类(Why)

  • 一方面来说,内部类一般是继承自某个类或者实现某个接口,内部类的代码操作创建它的外围类的对象,所以可以认为内部类提供了某种进入外围类的窗口。
  • 另一方面,因为每个内部类都能独立的继承自一个(接口的)实现,所以内部类有效的实现了多继承,允许继承多个非接口类型(类或者抽象类)。

如果不是为了解决多重继承问题,那么可以采取别的方式编码,但是使用内部类:可以获得其他的一些特性:

  • 内部类可以有多个实例,每个实例都有自己的状态信息,并且与外围类对象的信息相互独立。
  • 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
  • 创建内部类对象的时刻并不依赖于外围类的对象的创建。
  • 内部类并没有令人迷惑的“is a”关系,是一个独立的实体。

接下来,我们来看一下,如何使用内部类(how)

创建内部类

这里我把java编程思想中提到的细节列举一下:

首先是如何定义内部类。这个没什么可说的,就是把类的定义置于外围类里面。

再来看内部类如何链接到外部类

  1. 当生成一个内部类的对象时,此对象与制造它的外围对象之间就有了一种联系,所以他能访问其外围对象的所有成员,而不需要任何特殊条件,此外,内部类还拥有其外围类的所有元素的访问权。

  2. 那么内部类是如何做到拥有外围类所有成员的访问权呢?

        当某个外围类对象创建了一个内部类对象时,此内部类会秘密地捕获一个指向那个外围类对象的引用,然后,访问此外围类的成员时,就是用那个引用来选择外围类的成员,编译器为我们完成所有细节,所以说,构建内部类,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。

public interface Selector {
    boolean end();

    Object current();

    void next();
}

public class Sequence {

    private Object[] items;
    private int next = 0;

    public Sequence(int size) {
        items = new Object[size];
    }

    public void add(Object x){
        if (next < items.length){
            items[next++] = x;
        }
    }

    private class SequenceSelector implements Selector{

        private int i = 0;
        @Override
        public boolean end() {
            return i == items.length;
        }

        @Override
        public Object current() {
            return items[i];
        }

        @Override
        public void next() {
            if(i < items.length){
                i++;
            }
        }
    }
    public Selector selector(){
        return new SequenceSelector();
    }

    public static void main(String[] args) {
        Sequence sequence = new Sequence(10);
        for (int i = 0; i < 10; i++) {
            sequence.add(Integer.toString(i));
        }
        Selector selector = sequence.selector();
        while (!selector.end()){
            System.out.println(selector.current() + " ");
            selector.next();
        }
    }
}

我们再来看看生成外部类对象引用和定义内部类的两个特殊语法

  1. 如果需要生成外部类对象的引用,可以在外部类的类名后面紧跟圆点和this。这样产生的引用自动的具有正确类型,👾这一点在编译期间就被知晓并受到检查。

  2. 如果想要创建某个内部类对象,必须在表达式中提供对外部类对象的引用,使用到了.new语法。

用法请参考下面代码。

public class DotThis {

    void f(){
        System.out.println("DotThis.f()");
    }

    public class Inner{
        public DotThis outer(){
            return DotThis.this;
        }
    }

    public Inner inner(){
        return new Inner();
    }

    public static void main(String[] args) {
        DotThis dt =new DotThis();
        Inner inner = dt.inner();
        DotThis outer = inner.outer();
        System.out.println(outer.equals(dt));
    }
}

public class DotNew {

    public class Inner{}

    public static void main(String[] args) {
        DotNew dotNew = new DotNew();
        Inner inner = dotNew.new Inner();
    }
}

这里再说一下内部类向上转型为基类或者接口如何实现接口实现的不可见性

    当将内部类向上转型为基类或者接口的时候,该接口的实现能够完全不可见,甚至不可用,得到的只是指向基类或者接口的引用,隐藏了具体的实现。我们来看看下面的代码。

public interface Contents {
    int value();
}

public interface Destination {
    String readLable();
}

public class Parcel2 {

    private class PContents implements Contents{

        private  int i = 11;
        @Override
        public int value() {
            return i;
        }
    }

    protected class PDestination implements Destination{

        private String label;

        private PDestination(String label) {
            this.label = label;
        }

        @Override
        public String readLable() {
            return label;
        }
    }

    public Contents contents(){
        return new PContents();
    }

    public  Destination destination(String s){
        return new PDestination(s);
    }
}

public class TestParcel2 {

    public static void main(String[] args) {

        Parcel2 parcel2 = new Parcel2();


        Contents contents = parcel2.contents();
        Destination dest = parcel2.destination("xxx");

//        PContents内部类时private的,所以除了Parcel2,其他类不能访问它,甚至不能向下转型成内部类,因为不能访问其名字。完全隐藏了实现的细节。
//        PConents conents = parcel2.new PConents();
    }
}

方法和作用域内的内部类

👀首先我们先来看看在方法和作用域内定义内部类的理由

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

下面通过局部内部类和匿名内部类来了解在方法和作用域内部的类

局部内部类

  1. 定义

在某个作用域或者方法定义的内部类称为局部内部类

看看下面这个例子:

public class LocalInnerClass01 {

    public void function(){
//        if条件作用域
        if(true){
            /*局部内部类 In*/
            class  In{
                public void print(){
                    System.out.println("局部内部类");
                }
            }
        }
//        In in = new In();     Illegal
    }
}

所以,不要狭义的认为局部内部类仅仅是方法中定义的内部类。
  1. 局部内部类的几个特点:
  • 局部内部类类似方法的局部变量,所以在外围类外部或者类的其他方法中不能(局部内部类的作用域之外,方法或域外部)访问这个类,但是这并不代表局部内部类的实例和创建它的方法中的局部变量具有相同的生命周期。
  • 👌不是类成员,所有不能有访问修饰符。
  • 😔不能在局部内部类中使用可变的局部变量,访问作用域内的局部变量,该局部变量必须用final修饰。
  • 可以访问外围类的成员变量,使用外围类名.this.成员名。如果是static方法,则只能访问static修饰的成员变量。
  • 可以使用final 或 abstract修饰。
/**
 * 局部内部类demo
 * Nesting  a class within a method
 * @author FuPingstar
 * @date 2019/6/29 15:05
 */
public class Pracel5 {
    public Destination destination(String s){
        class PDetination implements Destination{
            private String label;
            
            private  PDetination(String whereTo){
                this.label = whereTo;
            }

            @Override
            public String readLable() {
                return label;
            }
        }
        return new PDetination(s);
    }
}

PDetination类是destination方法的一部分,所以在Destination方法之外不能访问PDetination类,在destination方法中定义了PDetination类,并不意味着destination方法执行完毕了,PDetination的示例就不能用了。

匿名内部类

匿名内部类的创建格式如下:
new 父类构造器 (参数列表) | 实现接口()
  {
    //匿名内部类的类体部分
  }


匿名内部类必须要继承一个父类或者实现一个接口,并且也只能实现一个父类或者是实现一个接口,同时也没有class关键字,通过new表达式返回的引用直接被自动向上转型为对父类的引用。

匿名内部类只能被使用一次,因为创建匿名内部类的时候会立即创建一个该类的实例,类的定义会立即消失,所以匿名内部类不能被重复使用。

Thread类的匿名内部类实现

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.print(i + " ");
                }
            }
        };
        t.start();
    }
}
如何给匿名内部类传递参数:

如果基类的构造器需要传递参数,只需要传递合适的参数给基类的构造器即可。看下面的例子:

/**
 *
 * anonymoud inner class demo
 * @author FuPingstar
 * @date 2019/6/29 16:50
 */
public class Parcel6 {

    public Wrapping wrapping(int x){
        return new Wrapping(x){
            public int valu1e(){
                return super.value1() * 47;
            }
        };
    }
}

在匿名类中,如果希望🎃使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的。看下面的例子:

public class Parcel7 {

    public Destination desination(final String test){
        return new Destination() {
            private String label = test;
            @Override
            public String readLable() {
                return label;
            }
        };
    }

    public static void main(String[] args) {
        Parcel7 parcel7 = new Parcel7();
        Destination xxx = parcel7.desination("xxx");
    }
}
匿名内部类初始化

匿名内部类没有构造器,所以可以使用实例初始化来达到构造器的效果。

/**
 * @author FuPingstar
 * @date 2019/6/29 20:11
 */
public class Parcel8 {

    public Destination destination(final String dest, final float price){
        return new Destination() {
            private int cost;
//            Instance initialization for each object:
            {
                cost = Math.round(price);
                if(cost > 100){
                    System.out.println("Over budeget");
                }
            }
            private String label = dest;

            @Override
            public String readLable() {
                return label;
            }
        };
    }


    public static void main(String[] args) {
        Parcel8 parcel8 = new Parcel8();
        Destination xxx = parcel8.destination("xxx", 101.395F);
    }
}
使用匿名内部类时应该注意的问题:
  1. 使用匿名内部类时,必须继承一个类或者是实现一个接口,并且两者不可兼得。
  2. 匿名内部类没有构造函数,通过实例初始化来达到构造器的效果。
  3. 匿名内部类不能存在任何的静态成员变量和静态方法。
  4. 匿名内部类是特殊的局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
  5. 匿名内部类不能是抽象的,必须实现继承的类和实现的接口的所有抽象方法。

嵌套类

将内部类声明为static时通常称为嵌套类。

普通的内部类对象隐式的保存了一个指向创建它的外围类的对象的引用,当内部类时static时,情况就变了。

嵌套类的特点
  • 要创建嵌套类的对象,并不需要其外围类的对象。
  • 不能从嵌套类的对象中访问非静态的外围类对象。《基于java语法:静态方法不能直接访问非静态成员》。
  • 普通内部类的字段与方法,只能放在类的外部层次上,所以普通内部类不能有static数据和static字段,也不能包含嵌套类,但是嵌套类可以包含这些东西。

嵌套类具体用法请参考如下代码:

以下代码嵌套类没有继承接口,只为探讨嵌套类区别于内部类的用法

public class NestedClassTest {

    private static int id = 111;
    private String name = "fupingstar";

    private static class NestedClass1{
        private int nid1 = 222;

        public int value(){
            return nid1;
        }
//       嵌套类不能访问外围类非静态成员变量
//        public String name(){
//            return name;
//        }
    }

    public class InnerClass1 {

        NestedClassTest getNestedClass(){
//            非静态内部类通过 .this语法获取指向创建它的外围类的引用
            return NestedClassTest.this;
        }
    }

    protected static class NestedClass2{
        public static void f() {}

        static int nid2 = 333;

        static class NestedClass3 {
            public static void f() {}

            static int nid3 = 444;
        }
    }


    public static NestedClass1 getNestedClass1(){
        return new NestedClass1();
    }

    public static NestedClass2 getNestedClass2(){
        return new NestedClass2();
    }

    public static void main(String[] args) {
        NestedClass1 nestedClass1 = getNestedClass1();

        NestedClass2 nestedClass2 = getNestedClass2();

        NestedClassTest nestedClassTest = new NestedClassTest();

        System.out.println(nestedClass1.nid1);

//        外围类使用 .new关键字创建内部类对象
        InnerClass1 innerClass1 = nestedClassTest.new InnerClass1();
        System.out.println(innerClass1.getNestedClass().toString());
    }
}

接口内部的类

嵌套类可以作为接口的一部分,放到接口中的任何类都自动是public和static的。因为类是static的,只是将嵌套类置于接口的命名空间内,不违反接口的规则。

一个最大的好处就是如果想让接口的所有不同实现公用一些公共代码,可以使用接口的嵌套类来解决这个问题。

Java编程思想的作者建议在每一个类中都写一个main方法来测试该类,这样做的缺点就是必须带着那些已编译的额外代码,所以可以用嵌套类来放置测试代码,这样会生成一个独立的内部类,用这个类来做测试即可,只需要在产品发布打包之前简单的删除内部类的字节码即可。

从多层嵌套类中访问外围类的成员

一个内部类被嵌套多少层都能够透明的访问所有它嵌入的外围类的所有成员。

用法参考以下代码:

public class NestedClassTest01 {

    private void f() {}

    class A {
        private void g() {}
        public class B {
            void h() {
                g();
                f();
            }
        }
    }

    public static class TestInnerClass{
        public static void main(String[] args) {
            NestedClassTest01 nestedClassTest01 = new NestedClassTest01();

            A a = nestedClassTest01.new A();
            A.B b = a.new B();
            b.h();
        }
    }
}

完结

这篇文章基本上阐述了内部类的语法和语义,对接口和内部类的使用是设计上的权衡问题,相信读者在以后的开发过程中对这两者的选用问题会慢慢明了,能够最终理解。下篇文章笔者会继续研究匿名内部类和闭包,Lambda表达式的联系。