第十章 内部类
- 将一个类的定义放在另一个类的内部,这就是内部类
- 内部类和组合是完全不同的概念,内部类了解外部类
10.1 创建内部类
创建就好,好像没啥可说的,直接来上练习:
练习1:(1)编写一个名为Outer的类,它包含一个名为Inner的类。在Outer中添加一个方法,它返回一个Inner类型的对象。在main()中,创建并初始化一个指向某个对象的引用。
public class Outer { public Inner getInnerInstance() { return new Inner(); } public static void main(String[] args) { Outer outer = new Outer(); Inner inner = outer.getInnerInstance(); } public class Inner { { System.out.println("创建Inner"); } } } 输出: 创建Inner
10.2 链接到外部类
所有内部类自动拥有对其外围类所有成员的访问权。内部类对象会秘密捕获一个指向那个外围类对象的引用。这些东西编译器都帮我们处理好了。直接来上练习:
练习2、创建一个类,持有一个String,将这个新类的对象添加到一个Sequence对象中,然后显示它们
练习3、修改练习1,使得Outer类包含一个private String域,Inner包含一个显示这个域的toString()方法,创建一个Inner类型的对象并显示它
这两个练习比较简单,略
10.3 使用.this与.new
如果需要生成对外部类的引用,直接外部类的名字后面紧跟圆点和this
要想直接创建内部类对象,不能按照我们想象的方式,必须使用外部类的对象来创建该内部类对象,在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到创建它的外部类对象上
练习4、生成对外部类Sequence的引用
练习5、创建一个包含内部类的类,在另一个独立的类中,创建此内部类的实例
还是比较简单,贯彻落实使用.this和.new就OK, 略
10.4 内部类与向上转型
内部类向上转型为其基类,能够很方便地隐藏实现细节,直接上练习:
练习6:(2)在第一个包中创建一个至少有一个方法的接口。然后在第二个包内创建一个类,在其中添加一个protected的内部类以实现那个接口。在第三个包中,继承这个类,并在一个方法中返回该protected内部类的对象,在返回的时候向上转型为第一个包中的接口的类型。
// 第一个包 package one1; public interface SimpleInterface { void eat(); } // 第二个包 package two; public class TestParent { protected class Inner implements SimpleInterface { public Inner() { System.out.println("创建了内部类"); } @Override public void eat() { System.out.println("吃东西"); } } } // 第三个包 package one3; public class Test extends TestParent{ public SimpleInterface getInner() { return new Inner(); } public static void main(String[] args) { new Test().getInner().eat(); } } 输出: 创建了内部类 吃东西
练习7:(2)创建一个含有private域和private方法的类。创建一个内部类,它有一个方法可以用来修改外围类的域,并调用外围类的方法。在外围类的另一方法中,创建此内部类的对象,并且调用它的方法,然后说明对外围类对象的影响。
// 这个练习显示了内部类具有对外部类的透明访问,甚至是私有域和方法 public class Test { private int count = 22;; private void onUseOuter() { System.out.println("外部类被调用了"); } public Inner getInnerInstance() { return new Inner(); } private class Inner { public void changeValue(int i) { count = i; System.out.println(i); onUseOuter(); } } public static void main(String[] args) { Test test = new Test(); Inner inner = test.getInnerInstance(); inner.changeValue(33); } } 输出: 33 外部类被调用了
练习8:(2)确定外部类是否可以访问其内部类的private元素
// 此练习显示外部类也可以访问内部类的private元素 public class Test2 { private class Inner { private int count = 22; } private void changeInnerValue(int i) { Inner inner = new Inner(); inner.count = i; System.out.println("改变后的值为" + inner.count); } public static void main(String[] args) { Test2 test2 = new Test2(); test2.changeInnerValue(55); } } 输出: 改变后的值为55
10.5 在方法和作用域内的内部类
以前没接触过,也没这么写过。但是,在方法和作用域中使用内部类,是一种更加难以理解的技术
这么做有两个理由:
- 创建某类型的接口,创建并返回对其的引用
- 要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但又不希望这个类是公共可用的
直接上练习吧:
练习9:(1)创建一个至少有一个方法的接口。在某个方法内定义一个内部类以实现此接口,这个方法返回对此接口的引用。
public class Test3 { private SimpleInterface get() { class simpleImpl implements SimpleInterface { public simpleImpl() { System.out.println("创建了一个方法内定义的内部类"); } @Override public void f() { System.out.println("接口的f()方法"); } } return new simpleImpl(); } public static void main(String[] args) { SimpleInterface simpleInterface = new Test3().get(); } public interface SimpleInterface { void f(); } }
练习10:(1)重复前一个练习,但将内部类定义在某个方法的一个作用域内。
// 和练习9的区别就是这个方法,加个if private SimpleInterface get() { if(true) { class simpleImpl implements SimpleInterface { public simpleImpl() { System.out.println("创建了一个方法内定义的内部类"); } @Override public void f() { System.out.println("接口的f()方法"); } } return new simpleImpl(); } return null; }
练习11:(2)创建一个private内部类,让它实现一个public接口。写一个方法,它返回一个指向此private内部类的实例的引用,并将此引用向上转型为该接口类型。通过尝试向下转型,说明此内部类被完全隐藏了。
// 此练习显示,因为内部类是私有的,只能向上转型,返回可见的基类 public class Test4 { private class Inner implements SimpleInterface { @Override public void f() { } } public SimpleInterface get() { return new Inner(); } public Inner get2() { return new Inner(); } public interface SimpleInterface { void f(); } } public class Test5 { public static void main(String[] args) { Test4 test4 = new Test4(); SimpleInterface simpleInterface = test4.get(); simpleInterface = test4.get2(); // 下面的直接报错,编译不通过 // Inner i1 = test4.get2(); // Inner i2 = (Inner)simpleInterface; } } // 注意此例子必须再其他类中的main()方法中 // 如果仍然在Test4中的main()方法就会编译通过
10.6 匿名内部类
// 某个方法返回一个匿名内部类对象 public Contents getContents() { return new Contents() { private int i = 11; public int value() { return i; } }; }
重点,也是我们在开发中常遇到的:
- 如果一个匿名内部类想使用一个外部定义的对象,那么编译器会要求这个参数引用必须是final的
- 匿名类中不可能有命名构造器
直接上练习:
练习12:(1)重复练习7
练习13:(2)重复练习9
练习14:
这三个略,练习9用匿名内部类写就是我们平时写的回调,更简单了
练习15:(2)创建一个类,它有非默认的构造器(即需要参数的构造器),并且没有默认构造器(没有无参数的构造器)。创建第二个类,它包含一个方法,能够返回对第一个类的对象的引用。通过写一个继承自第一个类的匿名内部类,来创建一个返回对象。
public class NoDefault { private int i; public NoDefault(int i) { this.i = i; } public void f() { System.out.println("NoDefault"); } } public class Pratice15 { public NoDefault get(int i) { return new NoDefault(i) {}; } public NoDefault get2(int i) { return new NoDefault(i) { public void f() { System.out.println("NoDefault匿名内部类"); } }; } public static void main(String[] args) { Pratice15 pratice15 = new Pratice15(); NoDefault noDefault = pratice15.get(2); noDefault.f(); noDefault = pratice15.get2(3); noDefault.f(); } } 输出: NoDefault NoDefault匿名内部类
10.6.1 再访工厂方法
使用工厂方法,声明一个static的工厂匿名内部类,返回当前类的对象。感觉十分的美妙。
练习16:(1)修改第9章中练习18的解决方案,让它使用匿名内部类
interface Cycle { int wheels(); } interface CycleFactory { Cycle getCycle(); } class Unicycle implements Cycle { public int wheels() { return 1; } public static CycleFactory factory = new CycleFactory() { public Unicycle getCycle() { return new Unicycle(); } }; } class Bicycle implements Cycle { public int wheels() { return 2; } public static CycleFactory factory = new CycleFactory() { public Bicycle getCycle() { return new Bicycle(); } }; } class Tricycle implements Cycle { public int wheels() { return 3; } public static CycleFactory factory = new CycleFactory() { public Tricycle getCycle() { return new Tricycle(); } }; } public class Pratice16 { public static void ride(CycleFactory fact) { Cycle c = fact.getCycle(); System.out.println(c.wheels()); } public static void main(String[] args) { ride(Unicycle.factory); ride(Bicycle.factory); ride(Tricycle.factory); } }
练习17:略
10.7 嵌套类
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static。
嵌套类意味着:
- 要创建嵌套类的对象,并不需要其外围类的对象。
- 不能从嵌套类的对象中访问非静态的外围类对象。
- 嵌套类的内部可以包含static数据,包含嵌套类
练习18:略
练习19:略
10.7.1 接口内部的类
嵌套类可以作为接口的一部分,甚至可以在内部类中实现其外围接口。
比如也可以在嵌套类中写一个main函数
练习20:略
练习21:略
10.7.2 从多层嵌套类中访问外部类的成员
一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外围类的所有成员。
10.8 为什么需要内部类
内部类最吸引人的原因是:
每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
内部类使得多重继承的解决方案变得完整。内部类有效地实现了“多重继承”。
使用内部类还可以获得其他一些特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
- 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
- 创建内部类对象的时刻并不依赖于外围类对象的创建
- 内部类并没有令人迷惑的 “is-a” 关系;它就是一个独立的实体
练习22:(2)实现Sequence.java中的reverseSelector()方法
// 改编一个吧,最简单的,猫狗啥的,哈哈哈 // 因为每个内部类都可以独立的实现一个接口, // 内部类最重要突出的功能就是实现同一个接口, // 很容易就能拥有另一个方法,暂时可以这么粗鄙的理解不知道合适不,哈哈哈 public class Pratice17 { private class Dog implements Animal { @Override public void eat() { System.out.println("吃骨头"); } } private class Cat implements Animal { @Override public void eat() { System.out.println("吃鱼"); } } public Animal getDog() { return new Dog(); } public Animal getCat() { return new Cat(); } public interface Animal { void eat(); } public static void eatFood(Animal animal) { animal.eat(); } public static void main(String[] args) { Pratice17 pratice17 = new Pratice17(); eatFood(pratice17.getDog()); eatFood(pratice17.getCat()); } }
练习23:
创建一个接口U,包含三个方法。
创建一个类A,包含一个方法,在此方法中通过创建一个匿名内部类来生成一个指向U的引用。
创建一个类B,包含一个由U组成的数组。B有几个方法,第一个方法接受U的引用,并存储的数组中。第二个方法将数组的引用设置为null,第三个方法遍历此数组,并在U中调用这些方法。
在main()中,创建一组A的对象和一个B的对象。用A类对象产生的U类型的引用填充B对象的数组。使用B回调所有A的对象。再从B中移除某些U的引用。
public interface U { void f(); void g(); void h(); } public class A { String name; public A(String name) { this.name = name; } public U getU() { return new U() { @Override public void f() { System.out.println(name + "f()"); } @Override public void g() { System.out.println(name + "g()"); } @Override public void h() { System.out.println(name + "h()"); } }; } } public class B { U[] array; public B(int size) { array = new U[size]; } public boolean add(U u) { for(int i = 0; i < array.length; i++) { if (array[i] == null) { array[i] = u; return true; } } return false; } public boolean setNull(int i) { if (i < 0 || i >= array.length) { return false; } array[i] = null; return true; } public void callMethods() { for (int i = 0; i < array.length; i++) { if (array[i] != null) { array[i].f(); array[i].g(); array[i].h(); } } } } public class Test { public static void main(String[] args) { A[] aa = {new A("哈登"), new A("保罗"), new A("卡佩拉")}; B b = new B(3); for (int i = 0; i < aa.length; i++) { b.add(aa[i].getU()); } System.out.println("所有的--------------------"); b.callMethods(); System.out.println("移除后--------------------\n"); b.setNull(1); b.setNull(2); b.callMethods(); } } // 输出 所有的-------------------- 哈登f() 哈登g() 哈登h() 保罗f() 保罗g() 保罗h() 卡佩拉f() 卡佩拉g() 卡佩拉h() 移除后-------------------- 哈登f() 哈登g() 哈登h()
10.8.1 闭包与回调
闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。
那么基于这个定义,内部类是面向对象的闭包。
在我们做Android开发的时候,各种点击事件,各种网络回调,很常见的,在匿名内部类当中我们可以随意的使用当前类的成员变量。我想这样更容易理解。
书中的例子就不说了,讲的很好,但是没有一定的实践和经验也不好理解。
Java更加小心仔细,没有在语言中使用指针,而使用了回调。
内部类提供的闭包功能是优良的解决方案,它比指针更灵活、更安全。
10.8.2 内部类与控制框架
这节内容挺抽象的其实,哈哈,控制框架,用来解决响应事件的需求
书中的例子,Event构造方法中传入延迟的时间,start()方法获得结束时间,ready()方法返回是否到达时间,抽象出一个action()去做动作,Controller()来控制
反正注意内部类的两点吧:
- 内部类将实现的细节封装了起来
- 内部类能够很容易地访问外围类的任意成员,这一点很重要
控制温室的运作,这个例子里面充满了内部类,其实也不用细看,做andorid的,点击事件匿名内部类是经常的,要么写一个内部类
练习24:略
练习25:略
10.9 内部类的继承
这个在平时的开发也用的少
结合练习简单说一下吧
练习26:创建一个包含内部类的类,此内部类有一个非默认构造器(需要的构造器 ),创建另一个也包含内部类的类,此内部类继承自第一个内部类。
public class WithNonDefault { class Inner{ int i; public Inner(int i) { this.i = i; } public Inner() { i = 47; } public void say() { System.out.println("父内部类的方法"); } } } public class Test { class InnerSon extends WithNonDefault.Inner { // 这个直接报错了,也就是说,要想继承一个内部类,必须在构造方法传入此内部类的外围类的引用 // public InnerSon(int i) { // super(i); // } public InnerSon(WithNonDefault wnd, int i) { wnd.super(i); } public void say() { System.out.println("子类的方法"); super.say(); } } public static void main(String[] args) { Test test = new Test(); InnerSon innerSon = test.new InnerSon(new WithNonDefault(),2); innerSon.say(); } }
10.10 内部类可以被覆盖吗
假设这样一个场景,创建一个内部类,继承外围类并定义此内部类时,会发生什么呢
// 鸡蛋和蛋黄 public class Egg { private Yolk yolk; public Egg() { System.out.println("创建了鸡蛋"); yolk = new Yolk(); } protected class Yolk { public Yolk() { System.out.println("创建了蛋黄"); } } } // 大鸡蛋继承鸡蛋 public class BigEgg extends Egg{ public class Yolk { public Yolk() { System.out.println("创建了大蛋黄"); } } public static void main(String[] args) { new BigEgg(); } } 输出: 创建了鸡蛋 创建了蛋黄 告诉我们一个道理: 类似这种像覆盖方法一样去覆盖内部类,是无法覆盖的 这两个内部类是完全独立的两个实体,各自在自己的命名空间内
明确的继承某个内部类是可以的:
package com__; public class Egg { private Yolk yolk = new Yolk(); public Egg() { System.out.println("创建了鸡蛋"); } public void insertYolk(Yolk y) { yolk = y; } public void changeYolk() { yolk.change(); } protected class Yolk { public Yolk() { System.out.println("创建了蛋黄"); } public void change() { System.err.println("蛋黄变了"); } } } public class BigEgg extends Egg{ public BigEgg() { insertYolk(new Yolk()); } public class Yolk extends Egg.Yolk { public Yolk() { System.out.println("创建了大蛋黄"); } @Override public void change() { System.out.println("大蛋黄变了"); } } public static void main(String[] args) { Egg egg = new BigEgg(); egg.changeYolk(); } } 输出: 创建了蛋黄 创建了鸡蛋 创建了蛋黄 创建了大蛋黄 大蛋黄变了
是这样执行的:
- new一个子类,肯定先执行父类的成员变量,先初始化Yolk,那么执行了Yolk的构造方法——创建了蛋黄
- 然后是父类的构造方法——创建了鸡蛋
- 然后是子类的构造方法,调用插入蛋黄的方法,又new了一个蛋黄,此蛋黄是子类的蛋黄,继承了父类中的蛋黄,所以new一个子类蛋黄,先执行父类构造方法——创建了蛋黄
- 然后再执行子类构造方法——创建了大蛋黄
- 最后调用变化的方法——执行变化方法的对象时子类蛋黄,所以——大蛋黄变了
正好借此机会在复习一下执行顺序这一块:
先不说继承,就是一个类正常的初始化:
- 肯定是先静态(静态成员和静态代码块谁在前谁先执行),将变量和代码块都看作是成员,同级的
- 然后非静态(非静态成员和非静态代码块也是谁在前谁先执行),将变量和代码块都看作是成员,同级的
- 最后是构造方法
继承的顺序是这样的:
继承的时候,记住一点,静态优先,所有就有了:
- 父类的静态成员,父类的静态代码块
- 子类的静态成员,子类的静态代码块
- 父类的成员,父类的非静态代码块,父类的构造方法
- 子类的成员,子类的非静态代码块,子类的构造方法
- 子类的有参构造方法没有super父类的构造方法,那么子类执行有参构造方法会默认调用父类的无参构造方法
10.11 局部内部类
局部内部类其实,哈哈,我没用过,用的比较少,一般都是匿名内部类
使用的方法,典型的方法就是在方法体内创建,局部内部类不能有访问修饰符,可以访问当前方法的常量,和外围类的所有成员
// 举个例子 public class Test1 { // 局部内部类,使用局部内部了的唯一理由是,我们需要一个已命名的构造器,或者需要重载构造器 public Count getCount() { class LocalCount implements Count { public LocalCount() { // 重载构造器 } public int next() { return 1; } } return new LocalCount(); } // 匿名内部类,只能实例初始化 public Count getCount1() { return new Count() { { // 不过匿名内部类可以有代码块,这个操作以前没考虑过,哈哈 } @Override public int next() { return 0; } }; } public interface Count { int next(); } }
10.12 内部类标识符
每个类都会生产.class文件,匿名内部类会在$后跟个数字,如果是内部类嵌套在别的内部类当中,名字加在外围类名字与“$”的后面
10.13 总结
接口和内部类其实挺抽象挺复杂的,这两者结合起来能解决C++中使用多重继承所能解决的问题。
什么时候使用接口,什么时候使用内部类,或者两者同时使用,我们自己熟悉了以后,去识别这些情况