Think in Java 5th/On Java 8阅读整理:内部类(篇2)

此文章为本系列的第二篇,继续内部类部分,GO!

书籍电子版传送门

无底色文字:原文章
有底色文字:个人备注
代码:原文代码+个人测试代码+个人注释


首先把上一篇最后坑填一下,也就是静态内部类在创建时不需要外部类对象引用。

分为以下四类进行验证:

1. 外部类的静态方法创建静态内部类对象(确实不需要外部类对象引用:StatContents statContents = new StatContents();)
2. 外部类的静态方法创建静态内部类对象(确实不需要外部类对象引用:StatContents statContents = new StatContents();)
3. 其他类的静态方法创建静态内部类对象(确实不需要外部类对象引用: Parcel2.StatContents statContents = new Parcel2.StatContents();)
4. 其他类的普通方法创建静态内部类对象(确实不需要外部类对象引用:Parcel2.StatContents statContents = new Parcel2.StatContents();)

以下为测试的代码:

// innerclasses/Parcel2.java
// Returning a reference to an inner class
public class Parcel2 {
     

    /**
     * @author: sunzhen
     * @date: 2019-08-29 19:10
     * @param: null
     * @return:
     * @throws:
     * @description: 测试静态方法使用内部类
    */
    public static void test1(){
     
        //外部类的静态方法中,创建内部类对象时,这个变量不需要加外部类名称的前缀(参考TestInnerClazz类的其他类创建内部类的例子)
        /*Contents contents = new Contents(); 不能直接创建,除非内部类也为static,或将该方法的static去除*/
        // 1. 接收
        //Contents contents = new Parcel2().contents();
        // 2. 利用.new语法创建内部类对象
        Contents contents = new Parcel2().new Contents();
        System.out.println(contents.value());

    }

    /**
     * @author: sunzhen
     * @date: 2019-08-29 19:10
     * @param: null
     * @return:
     * @throws:
     * @description: 测试静态方法使用静态内部类
     */
    public static void testStatInner1(){
     
        //与普通方法一样都可以直接创建静态内部类
        StatContents statContents = new StatContents();
        System.out.println(statContents.value());

    }

    /**
     * @author: sunzhen
     * @date: 2019-08-29 19:10
     * @param: null
     * @return:
     * @throws:
     * @description: 测试普通方法使用静态内部类
     */
    public void testStatInner2(){
     
        //与普通方法一样都可以直接创建静态内部类
        StatContents statContents = new StatContents();
        System.out.println(statContents.value());
    }

    static class StatContents {
     
        private int i = 33;

        public int value() {
      return i; }
    }

    class Contents {
     
        private int i = 11;

        public int value() {
      return i; }
    }

    class Destination {
     
        private String label;

        Destination(String whereTo) {
     
            label = whereTo;
        }

        String readLabel() {
      return label; }
    }


    public Destination to(String s) {
     
        //说明普通方法可以直接创建普通内部类对象
        return new Destination(s);
    }

    public Contents contents() {
     
        //说明普通方法可以直接创建普通内部类对象
        return new Contents();
    }

    public void ship(String dest) {
     
        Contents c = contents();
        Destination d = to(dest);
        System.out.println(d.readLabel());
    }

    public static void main(String[] args) {
     
        //测试静态方法使用内部类,不需要在内部类类名加外部类类名前缀
        test1();
        //测试静态方法使用静态内部类,不需要在内部类类名加外部类类名前缀
        testStatInner1();
        //测试普通方法使用静态内部类,不需要在内部类类名加外部类类名前缀
        new Parcel2().testStatInner2();
    }
}
/**
 * @author: sunzhen
 * @date: 2019/8/29
 * @time: 19:22
 * @description: 配合Parcel2测试其内部类在该类(TestInnerClazz)中使用方式
 */
public class TestInnerClazz {
     

    /**
     * @author: sunzhen
     * @date: 2019-08-29 19:28
     * @param: null
     * @return:
     * @throws:
     * @description: 其他类静态方法创建Parcel2类的Contents内部类对象。
    */
    public static void test01(){
     
        Parcel2 parcel = new Parcel2();
        // 1. 接收
        //Parcel2.Contents contents = parcel.contents();
        // 2. 利用.new语法创建内部类对象
        //内部类变量名需要加外部类类名前缀,创建对象需要外部类对象引用。
        Parcel2.Contents contents = parcel.new Contents();
        System.out.println(contents.value());
    }

    /**
     * @author: sunzhen
     * @date: 2019-08-29 19:28
     * @param: null
     * @return:
     * @throws:
     * @description: 其他类普通方法创建Parcel2类的Contents内部类对象。
     */
    public void test02(){
     
        Parcel2 parcel = new Parcel2();
        // 1. 接收
        //Parcel2.Contents contents = parcel.contents();
        // 2. 利用.new语法创建内部类对象
        //内部类变量名需要加外部类类名前缀,创建对象需要外部类对象引用。
        Parcel2.Contents contents = parcel.new Contents();
        System.out.println(contents.value());
    }

    /**
     * @author: sunzhen
     * @date: 2019-08-29 19:28
     * @param: null
     * @return:
     * @throws:
     * @description: 其他类静态方法创建Parcel2类的StatContents静态内部类对象。
     */
    public static void test03(){
     
        //内部类类名需要加外部类类名前缀,可直接创建内部类对象。
        Parcel2.StatContents statContents = new Parcel2.StatContents();
        System.out.println(statContents.value());
    }

    /**
     * @author: sunzhen
     * @date: 2019-08-29 19:28
     * @param: null
     * @return:
     * @throws:
     * @description: 其他类普通方法创建Parcel2类的StatContents静态内部类对象。
     */
    public void test04(){
     
        //内部类类名需要加外部类类名前缀,可直接创建内部类对象。
        Parcel2.StatContents statContents = new Parcel2.StatContents();
        System.out.println(statContents.value());
    }

    public static void main(String[] args) {
     
        TestInnerClazz innerClazz = new TestInnerClazz();
        //其他类静态方法创建Parcel2类的Contents内部类对象。
        test01();
        //其他类普通方法创建Parcel2类的Contents内部类对象。
        innerClazz.test02();
        //其他类静态方法创建Parcel2类的StatContents静态内部类对象。
        test03();
        //其他类普通方法创建Parcel2类的StatContents静态内部类对象。
        innerClazz.test04();
    }

}

然后开始今天的内容


内部类与向上转型

  当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类-某个接口的实现-能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。

我们可以创建前一个示例的接口:

// innerclasses/Destination.java
public interface Destination {
     
    String readLabel();
}
// innerclasses/Contents.java
public interface Contents {
     
    int value();
}

现在 Contents 和 Destination 表示客户端程序员可用的接口。记住,接口的所有成员自动被设置为 public。

当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,看下面的例子:

// innerclasses/TestParcel.java
class Parcel4 {
     
    private class PContents implements Contents {
     
        private int i = 11;
        @Override
        public int value() {
      return i; }
    }
    protected final class PDestination implements Destination {
     
        private String label;
        private PDestination(String whereTo) {
     
            label = whereTo;
        }
        @Override
        public String readLabel() {
      return label; }
    }
    public Destination destination(String s) {
     
        return new PDestination(s);
    }
    public Contents contents() {
     
        return new PContents();
    }
}
public class TestParcel {
     
    public static void main(String[] args) {
     
        Parcel4 p = new Parcel4();
        Contents c = p.contents();
        Destination d = p.destination("Tasmania");
        // Illegal -- can't access private class:
        //- Parcel4.PContents pc = p.new PContents();
    }
}

  在 Parcel4 中,内部类 PContents 是 private,所以除了 Parcel4,没有人能访问它。普通(非内部)类的访问权限不能被设为 private 或者 protected;他们只能设置为 public 或 package 访问权限。

  PDestination 是 protected,所以只有 Parcel4 及其子类、还有与 Parcel4 同一个包中的类(因为 protected 也给予了包访问权)能访问 PDestination,其他类都不能访问 PDestination,这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上,甚至不能向下转型成 private 内部类(或 protected 内部类,除非是继承自它的子类),因为不能访问其名字,就像在 TestParcel 类中看到的那样。

  private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会。

  这节看是看懂了,也实践了,就是觉得实际运用比较少。本来一个单独的类要么是public的要么是deafult的,但是内部类可以private,隐藏内部实现细节,在某些情况下这种代码更加高效优雅吧,以后用到再回来看看

在方法和作用域内的内部类

  到目前为止,读者所看到的只是内部类的典型用途。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,简单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。

这么做有两个理由:

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

在后面的例子中,先前的代码将被修改,以用来实现:

  1. 一个定义在方法中的类。
  2. 一个定义在作用域内的类,此作用域在方法的内部。
  3. 一个实现了接口的匿名类。
  4. 一个匿名类,它扩展了没有默认构造器的类。
  5. 一个匿名类,它执行字段初始化。
  6. 一个匿名类,它通过实例初始化实现构造(匿名内部类不可能有构造器)。

  第一个例子展示了在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。这被称作局部内部类:

// innerclasses/Parcel5.java
// Nesting a class within a method
public class Parcel5 {
     
    public Destination destination(String s) {
     
        final class PDestination implements Destination {
     
            private String label;

            private PDestination(String whereTo) {
     
                label = whereTo;
            }

            @Override
            public String readLabel() {
      return label; }
        }
        return new PDestination(s);
    }

    public static void main(String[] args) {
     
        Parcel5 p = new Parcel5();
        Destination d = p.destination("Tasmania");
    }
}

  PDestination 类是 destination() 方法的一部分,而不是 Parcel5 的一部分。所以,在 destination() 之外不能访问 PDestination,注意出现在 return 语句中的向上转型-返回的是 Destination 的引用,它是 PDestination 的基类。当然,在 destination() 中定义了内部类 PDestination,并不意味着一旦 dest() 方法执行完毕,PDestination 就不可用了。

你可以在同一个子目录下的任意类中对某个内部类使用类标识符 PDestination,这并不会有命名冲突。

下面的例子展示了如何在任意的作用域内嵌入一个内部类:

// innerclasses/Parcel6.java
// Nesting a class within a scope
public class Parcel6 {
     
    private void internalTracking(boolean b) {
     
        if(b) {
     
            class TrackingSlip {
     
                private String id;
                TrackingSlip(String s) {
     
                    id = s;
                }
                String getSlip() {
      return id; }
            }
            TrackingSlip ts = new TrackingSlip("slip");
            String s = ts.getSlip();
        }
        // Can't use it here! Out of scope:
        //- TrackingSlip ts = new TrackingSlip("x");
    }
    public void track() {
      internalTracking(true); }
    public static void main(String[] args) {
     
        Parcel6 p = new Parcel6();
        p.track();
    }
}

  TrackingSlip 类被嵌入在 if 语句的作用域内,这并不是说该类的创建是有条件的,它其实与别的类一起编译过了。然而,在定义 Trackingslip 的作用域之外,它是不可用的,除此之外,它与普通的类一样。

  这节主要就是讲了方法中也可以具有类,就是方法内部类(也称:局部内部类),然后其作用域也只在方法中。

匿名内部类

下面的例子看起来有点奇怪:

// innerclasses/Parcel7.java
// Returning an instance of an anonymous inner class
public class Parcel7 {
     
    public Contents contents() {
     
        return new Contents() {
      // Insert class definition
            private int i = 11;

            @Override
            public int value() {
      return i; }
        }; // Semicolon required
    }

    public static void main(String[] args) {
     
        Parcel7 p = new Parcel7();
        Contents c = p.contents();
    }
}

  contents() 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个 Contents 对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。

  这种奇怪的语法指的是:“创建一个继承自 Contents 的匿名类的对象。”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。上述匿名内部类的语法是下述形式的简化形式:

// innerclasses/Parcel7b.java
// Expanded version of Parcel7.java
public class Parcel7b {
     
    class MyContents implements Contents {
     
        private int i = 11;
        @Override
        public int value() {
      return i; }
    }

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

    public static void main(String[] args) {
     
        Parcel7b p = new Parcel7b();
        Contents c = p.contents();
    }
}

  在这个匿名内部类中,使用了默认的构造器来生成 Contents。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:

// innerclasses/Parcel8.java
// Calling the base-class constructor
public class Parcel8 {
     
    public Wrapping wrapping(int x) {
     
        // Base constructor call:
        return new Wrapping(x) {
      // [1]
            @Override
            public int value() {
     
                return super.value() * 47;
            }
        }; // [2]
    }
    public static void main(String[] args) {
     
        Parcel8 p = new Parcel8();
        Wrapping w = p.wrapping(10);
    }
}

[1] 将合适的参数传递给基类的构造器。
[2] 在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。

尽管 Wrapping 只是一个具有具体实现的普通类,但它还是被导出类当作公共“接口”来使用。

// innerclasses/Wrapping.java
public class Wrapping {
     
    private int i;
    public Wrapping(int x) {
      i = x; }
    public int value() {
      return i; }
}

为了多样性,Wrapping 拥有一个要求传递一个参数的构造器。

在匿名类中定义字段时,还能够对其执行初始化操作:

// innerclasses/Parcel9.java
public class Parcel9 {
     
    // Argument must be final or "effectively final"
    // to use within the anonymous inner class:
    public Destination destination(final String dest) {
     
        return new Destination() {
     
            private String label = dest;
            @Override
            public String readLabel() {
      return label; }
        };
    }
    public static void main(String[] args) {
     
        Parcel9 p = new Parcel9();
        Destination d = p.destination("Tasmania");
    }
}

  如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的(也就是说,它在初始化后不会改变,所以可以被当作 final),就像你在 destination() 的参数中看到的那样。这里省略掉 final 也没问题,但是通常最好加上 final 作为一种暗示。

  如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:

// innerclasses/AnonymousConstructor.java
// Creating a constructor for an anonymous inner class
abstract class Base {
     
    Base(int i) {
     
        System.out.println("Base constructor, i = " + i);
    }
    public abstract void f();
}
public class AnonymousConstructor {
     
    public static Base getBase(int i) {
     
        return new Base(i) {
     
            {
      System.out.println(
                    "Inside instance initializer"); }
            @Override
            public void f() {
     
                System.out.println("In anonymous f()");
            }
        };
    }
    public static void main(String[] args) {
     
        Base base = getBase(47);
        base.f();
    }
}

输出为:

Base constructor, i = 47
Inside instance initializer
In anonymous f()

  在此例中,不要求变量一定是 final 的。因为被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。

  下例是带实例初始化的"parcel"形式。注意 destination() 的参数必须是 final 的,因为它们是在匿名类内部使用的(译者注:即使不加 final, Java 8 的编译器也会为我们自动加上 final,以保证数据的一致性)。

// innerclasses/Parcel10.java
// Using "instance initialization" to perform
// construction on an anonymous inner class
public class Parcel10 {
     
    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 budget!");
            }
            private String label = dest;
            @Override
            public String readLabel() {
      return label; }
        };
    }
    public static void main(String[] args) {
     
        Parcel10 p = new Parcel10();
        Destination d = p.destination("Tasmania", 101.395F);
    }
}

输出为:

Over budget!

  在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是 if 语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。

  匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。

匿名内部类相较于其他用法还是比较常见的,适合于只用到一次的类。如果定义一个匿名内部类,并想使用一个在其外部定义的对象,那么参数需要时final的,不过 Java 8 可以不加,因为 Java 8 的编译器会为其自动加上 final,以保证数据的一致性

你可能感兴趣的:(读书心得,局部内部类,匿名内部类,作用域,向上转型)