TJI读书笔记13-内部类
我最恶心的两部分内容,一个叫lambda表达式,一个叫内部类. 智商有限,一直理解不了…
简单来说,将一个类的定义放在另一个类的内部就是内部类. 内部类的好处在于,将一些逻辑上相关的类组织在一起,并控制位于内部的类的可视性.内部类可以了解外围类,并与之通信. 但是内部类和组合是两个概念,比如将人的大腿定义在人这个类的内部,这就是一个比较合理的设定.如果通过组合,也可以实现,但是总是有那么一丢丢别扭的. 因为大腿脱离了人,几乎就没用任何意义了.
创建内部类
来吧,一码解千愁
1.public class Parcel1 {
2. //a inner class
3. class Contents{
4. private int i = 11;
5. public int value(){
6. return i;
7. }
8. }
9.
10. //a inner class
11. class Destination{
12. private String label;
13. public Destination(String whereTo) {
14. label=whereTo;
15. }
16. String readLabel(){
17. return label;
18. }
19. }
20.
21. public void ship(String dest){
22. Contents c = new Contents();
23. Destination d = new Destination(dest);
24. System.out.println(c.value());
25. System.out.println(d.readLabel());
26. }
27. public static void main(String[] args) {
28. Parcel1 p = new Parcel1();
29. p.ship("USA");
30.
31. }
32.}
这是一个非常简单的应用,就像使用其他普通的类一样去使用内部类.更典型的情况,在外部类中提供一个方法返回一个指向内部类实例的引用.
1.public class Parcel2 {
2. class Contents {
3. private int i = 11;
4.
5. public int value() {
6. return i;
7. }
8. }
9.
10. class Destination {
11. private String label;
12.
13. public Destination(String whereTo) {
14. label = whereTo;
15. }
16.
17. String readLabel() {
18. return label;
19. }
20. }
21.
22. public Destination to(String s) {
23. return new Destination(s);
24. }
25.
26. public Contents contents() {
27. return new Contents();
28. }
29.
30. public void ship(String dest) {
31. Contents c = this.contents();
32. Destination d = this.to(dest);
33. System.out.println(d.readLabel());
34. }
35.
36. public static void main(String[] args) {
37. Parcel2 p1 = new Parcel2();
38. p1.ship("USA");
39.
40. // define reference to inner classes
41. Parcel2 p2 = new Parcel2();
42. Parcel2.Contents c = p2.contents();
43. Parcel2.Destination d = p2.to("UK");
44. System.out.println(c.value());
45. System.out.println(d.readLabel());
46. }
47.}
注意,22行,26行定义了两个方法用来返回内部类的实例. 通过这种方式可以更好的做校验逻辑. 另外,还有42-43行,在外部类的非静态方法之外的地方创建内部类实例的时候,需要用OuterClassName.InnerClassName
的方式具体指明对象的类型.
突然想到一个问题,内部类在加载过程中是个什么样子的?
内部类和外部类的关系
从前面一小节,我们可以得知内部类的一个作用:对外隐藏名称空间和组织代码. 这一节唠唠一个更重要的特性,内部类可以无条件的访问外部嵌套类的所有成员.内部类的可视范围是它的直接外嵌类,也就是说内部类其实是拥有两个this指针,一个指向它自己的实例本身,另外一个指向外嵌类的实例.
1.interface Selector {
2. //check if the sequence cursor goto end.
3. boolean end();
4. //return the object where the current cursor pointed
5. Object current();
6. //move the cursor to next element
7. void next();
8.}
9.
10.public class Sequence {
11. private Object[] items;
12. private int next = 0;
13. public Sequence(int size){
14. items = new Object[size];
15. }
16. public void add(Object x){
17. if(next<items.length){
18. items[next++] = x;
19. }
20. }
21. private class SequenceSelector implements Selector{
22. private int i =0;
23. public boolean end(){
24. return i == items.length;
25. }
26. public Object current(){
27. return items[i];
28. }
29. public void next(){
30. if(i<items.length) i++;
31. }
32. }
33.
34. public Selector selector(){
35. return new SequenceSelector();
36. }
37. public static void main(String[] args) {
38. Sequence sequence = new Sequence(10);
39. for(int i =0;i<10;i++){
40. sequence.add(Integer.toString(i));
41. }
42. Selector selector = sequence.selector();
43. while(!selector.end()){
44. System.out.print(selector.current()+" ");
45. selector.next();
46. }
47. }
48.}
这是一个典型的迭代器原型,注意,24,27,30行均访问了外嵌类的成员 items. 这个是可以的. 说明内部类可以访问外嵌类成员.可以说内部类使用外嵌类的成员时,就像访问它自己的成员一样. 也就是说内部类 自动拥有其外嵌类所有成员的访问权,这是怎么做到的呢?埃大爷说,当外嵌类实例创建内部类实例的时候,此内部类对象必定会捕获一个指向外嵌类对象的引用. 当我们访问外嵌类的成员的时候,就是使用那个引用来选择外嵌类的成员. 这一切都是隐式完成的,我们不需要操心. 其实也就是开头的时候说的,内部类会拥有两个this指针.
还有一个地方需要注意,第21行,内部类使用的private修饰符. 这个表示在外嵌类之外,内部类不可见. 也就是说内部类是可以使用private修饰的. 但是不要忘了,普通的类是不可以使用private修饰.
.this和.new
.this
如果需要在内部类中获取外嵌类的引用,可以使用OuterClassName.this
的方式来获取.
1.public class DotThis {
2. void f(){
3. System.out.println("DoThis.f()");
4. }
5. public class Inner{
6. public DotThis outer(){
7. return DotThis.this;
8. }
9. }
10. public Inner inner(){
11. return new Inner();
12. }
13. public static void main(String[] args) {
14. DotThis dt = new DotThis();
15. DotThis.Inner dti = dt.inner();
16. dti.outer().f();
17.
18. System.out.println(dt==dti.outer());
19. }
20.}/*output:
21.DoThis.f()
22.true
23.*/
第7行展示了这种方式的用法. 而且从结果中也可以看到,拿到的就是外嵌类实例的引用.
.new
如果想在外嵌类中直接创建某个内部类对象. 还是使用new表达式.但是有点小区别.
1.public class DotNew {
2. public class Inner{
3. String str = "I am a inner class";
4. public String getStr(){
5. return str;
6. }
7. }
8. public static void main(String[] args) {
9. DotNew dn = new DotNew();
10. DotNew.Inner dni = dn.new Inner();
11. System.out.println(dni.getStr());
12. }
13.}
正常创建一个类实例的时候是ClassName instanceName = new ClassConstructor()
. 创建内部类实例的时候其实也是这个样子. 只不过要知道如何找到确切的类名和构造器.第10行展示了如何直接创建内部类实例.
内部类和向上转型
之前说内部类可以很好的隐藏名称空间和代码组织. 但是之前的做法不够地道. 这里给出一个更地道的实现.
1.class Parcel4{
2. private class PContents implements Contents{
3. private int i = 11;
4. @Override
5. public int value() {
6. return i;
7. }
8. }
9. private class PDestination implements Destination{
10. private String label;
11. public PDestination(String whereTo) {
12. label=whereTo;
13. }
14. @Override
15. public String readLabel() {
16. return label;
17. }
18. }
19. public Destination destination(String dest){
20. return new PDestination(dest);
21. }
22. public Contents content(){
23. return new PContents();
24. }
25.}
26.public class TestParcel4 {
27.
28. public static void main(String[] args) {
29. Parcel4 p = new Parcel4();
30. Contents c = p.content();
31. Destination d = p.destination("USA");
32. System.out.println(c.value());
33. System.out.println(d.readLabel());
34. //can not access Parcel4's private member
35. //Parcel4.PContents pc = p.new PContents();
36. }
37.}
这里把内部类声明为private,然后给出两个public的方法来获取内部类. 但是,这个public的方法返回的是其接口类型.(也就是做了个向上转型) 这时实现某个接口的内部类对外是完全不可见的,甚至不知道其具体的类型. 这种隐藏就更彻底了. private的内部类给类的设计者提供了一种更强大的隐藏方式. 完全隐藏实现细节,甚至可以完全阻止任何依赖于类型的编码. 需要注意的一点是,这种方式,内部类做了向上转型,所以内部类中实现的扩展的接口是没有意义的.
局部内部类
前面所说的内部类都是以类似成员变量的方式定义的. 也就是直接定义在外嵌类的里面. 内部类还可以定义在方法内或者任意的作用域内,也就是传说中的局部内部类. (最蛋疼的一种内部类,没有之一)
1.public class Parcel5 {
2. public Destination destination(String s){
3. class PDestination implements Destination{
4. String label;
5. private PDestination(String whereTo) {
6. label = whereTo;
7. }
8. @Override
9. public String readLabel() {
10. return label;
11. }
12. }
13. return new PDestination(s);
14. }
15. public static void main(String[] args) {
16. Parcel5 p = new Parcel5();
17. Destination d = p.destination("USA");
18. System.out.println(d.readLabel());
19. }
20.}
在任意的作用于内嵌入内部类
1.public class Parcel6 {
2. private void internalTracking(boolean b) {
3. if(b) {
4. class TrackingSlip {
5. private String id;
6. TrackingSlip(String s) {
7. id = s;
8. }
9. String getSlip() { return id; }
10. }
11. TrackingSlip ts = new TrackingSlip("slip");
12. String s = ts.getSlip();
13. }
14. // Can't use it here! Out of scope:
15. //! TrackingSlip ts = new TrackingSlip("x");
16. }
17. public void track() { internalTracking(true); }
18. public static void main(String[] args) {
19. Parcel6 p = new Parcel6();
20. p.track();
21. }
22. } ///:~
这一节是最头疼的一节,看上去很好理解有不知道哪里会用到它. 埃大爷说了,定义这种内部类有两个理由:
- 实现了某个类型的接口,于是可以创建并返回对其的引用(啥意思没看懂…)
- 需要解决一个复杂的问题,想创建一个类来辅助解决,但是又不希望这个类是公共可用的. (那这个问题得多复杂,既然复杂度那么高,是不是可以再拆分一下问题模型呢…)
匿名内部类
局部内部类貌似使用的不是很多,而且很多时候是可以被匿名内部类代替的.
匿名内部类的定义和初始化
按照之前的设定,想使用一个内部类是这样写的.
1.public class Parcel7b {
2.
3. class MyContents implements Contents {
4. private int i = 11;
5. public int value() {
6. return i;
7. }
8. }
9. public Contents contents(){
10. return new MyContents();
11. }
12. public static void main(String[] args) {
13. Parcel7b p = new Parcel7b();
14. Contents c = p.contents();
15. System.out.println(c.value());
16. }
17.
18.}
我们的主要目的是使用contents方法来拿到一个关于实现了Contents接口的内部类的实例的引用. java对于这种场景给出了一种新的语法,让我们可以通过更少的代码实现这一功能. 像下面这样.
1.public class Parcel7 {
2. public Contents contents(){
3.
4. //anonymous innner class
5. return new Contents() {
6. int i = 11;
7. @Override
8. public int value() {
9. // TODO Auto-generated method stub
10. return i;
11. }
12. };
13. }
14.
15. public static void main(String[] args) {
16. Parcel7 p = new Parcel7();
17. Contents c = p.contents();
18. System.out.println(c.value());
19. }
20.}
当然也可以使用带参数的构造器. 这时候直接使用基类的构造器就可以了.
1.public class Parcel8 {
2. public Wrapping wrapping(int x) {
3. // Base constructor call:
4. return new Wrapping(x) { // Pass constructor argument.
5. public int value() {
6. return super.value() * 47;
7. }
8. }; // Semicolon required
9. }
10. public static void main(String[] args) {
11. Parcel8 p = new Parcel8();
12. Wrapping w = p.wrapping(10);
13. }
14. } ///:~
15.//=======================================================================
16.public class Wrapping {
17. private int i;
18. public Wrapping(int x) { i = x; }
19. public int value() { return i; }
20. } ///:~
在匿名类中初始化参数
1.
2.public class Parcel9 {
3. private int i =10;
4. public Destination destination(String dest){
5. //public Destination destination(final String dest){
6. return new Destination() {
7. private String label = dest;
8. int j =i;
9. @Override
10. public String readLabel() {
11. // TODO Auto-generated method stub
12. System.out.println(j);
13. return label;
14. }
15. };
16. }
17. public static void main(String[] args) {
18. Parcel9 p = new Parcel9();
19. Destination d = p.destination("USA");
20.
21. System.out.println(d.readLabel());
22. }
23.}
匿名内部类还是一个内部类,内部类的特性它还是有的,比如可以无条件的访问外嵌类的成员. 但是要求形参引用类型必须是final,但是我试了一下,1.6版本中还有这个限制,在1.8中没有这个限制了. 这种方法简单的给一个字段赋值是可以的,但是如果想要完成一些构造器的行为,怎么办呢?
首先匿名内部类中是不可能有命名构造器的. 但是通过使用实例初始化器可以达到类似的效果.
1.abstract class Base{
2. public Base(int i){
3. System.out.println("Base constructor, i= "+i);
4. }
5. public abstract void f();
6.}
7.
8.public class AnonymousConstructor {
9. public static Base getBase(int i){
10. return new Base(i){
11. {System.out.println("inside instance initializer");}
12. public void f(){
13. System.out.println("in anonymous f()");
14. }
15. };
16. }
17. public static void main(String[] args) {
18. Base base = AnonymousConstructor.getBase(47);
19. base.f();
20. }
21.}
再比如:
1.public class Parcel10 {
2. public Destination destination(String dest,float price){
3. return new Destination() {
4. private float cost;
5. {
6. cost = Math.round(price);
7. if(cost>100){
8. System.out.println("over budget");
9. }
10.
11. }
12. private String label = dest;
13. @Override
14. public String readLabel() {
15. // TODO Auto-generated method stub
16. return label;
17. }
18. };
19. }
20. public static void main(String[] args) {
21. Parcel10 p = new Parcel10();
22. p.destination("USA", 101.123f);
23. }
24.}
上面代码5-9行是一个实例初始化器,它的作用在匿名内部类中就相当于一个构造器. 但是匿名内部类中是有限制的,比如,构造器不能重载,而且只能继承一个类,或者实现一个接口,并不能两者兼备.
使用匿名内部类来实现工厂模式
1.interface Service {
2. void method1();
3.
4. void method2();
5.}
6.
7.interface ServiceFactory {
8. Service getService();
9.}
10.
11.class Implementation1 implements Service {
12. private Implementation1() {
13. }
14.
15. @Override
16. public void method1() {
17. System.out.println("I1m1");
18. }
19.
20. @Override
21. public void method2() {
22. System.out.println("I1m2");
23. }
24.
25. public static ServiceFactory factory() {
26. return new ServiceFactory() {
27. @Override
28. public Service getService() {
29. // TODO Auto-generated method stub
30. return new Implementation1();
31. }
32. };
33. }
34.
35.}
36.
37.class Implementation2 implements Service {
38. private Implementation2() {
39. }
40.
41. @Override
42. public void method1() {
43. System.out.println("I2m1");
44. }
45.
46. @Override
47. public void method2() {
48. System.out.println("I2m2");
49. }
50.
51. public static ServiceFactory factory() {
52. return new ServiceFactory() {
53. @Override
54. public Service getService() {
55. // TODO Auto-generated method stub
56. return new Implementation2();
57. }
58. };
59. }
60.
61.}
62.
63.public class Factories {
64. public static void serviceConsumer(ServiceFactory fact) {
65. Service s = fact.getService();
66. s.method1();
67. s.method2();
68. }
69. public static void main(String[] args) {
70. serviceConsumer(Implementation1.factory());
71. serviceConsumer(Implementation2.factory());
72. }
73.}
上一章中说到了工厂模式,那个时候还需要创建一个Implementation1Factory
的类,有了匿名内部类之后,这个就不需要创建了. 像25行,51行使用了匿名内部类.
嵌套类
如果不需要内部类对象与外围类对象之间有联系,那可以将内部类对象声明为static. 这样就有了嵌套类,也叫静态内部类. 非static修饰的内部类都会保有外嵌类的this指针. 但是嵌套内部类没有这个指针. 所以一个内嵌类看上去有点类似一个static方法. 埃大爷说:
- 要创建嵌套类的对象,不需要其外围类的对象.
- 不能从嵌套类对象中访问非静态的外围类的对象.
嵌套类与普通内部类还有一个区别,也是之前没有说道的一个限制. 普通的内部类中不能含有static成员.也就是所有static成员必须放在外嵌类中生命. 但是嵌套类中是可以声明static成员的.
1.public class Parcel11 {
2. //nested classes
3. private static class ParcelContents implements Contents{
4. private int i =11;
5. @Override
6. public int value() {
7. return i;
8. }
9. }
10.
11. protected static class ParcelDestination implements Destination{
12. String label;
13. private ParcelDestination(String whereTo) {
14. label = whereTo;
15. }
16. @Override
17. public String readLabel() {
18.
19. return label;
20. }
21. public static void f(){
22. System.out.println("f()");
23. }
24. //we can define static members in a nested class
25. static int x =10;
26. static class AnotherLevel{
27. public static void f(){
28. System.out.println("f()");
29. }
30. static int x =10;
31. }
32. }
33. public static Destination destination(String s){
34. return new ParcelDestination(s);
35. }
36. public static Contents contents(){
37. return new ParcelContents();
38. }
39.
40. public static void main(String[] args) {
41. Contents c = contents();
42. Destination d = destination("USA");
43. }
44.}
嵌套类大概就是这个样子,注意!!!
- main方法中并没有创建外嵌类的实例,但是依旧可以创建内嵌类的实例.
- 25,26行,在一个内嵌类的内部创建了static成员.
接口内部的类
我们来做一个不严谨的推导,所有类都可以创建内部类–>java支持内嵌类(说起来好别扭,感觉静态内部类听起来好听一点…)–>接口是一种特殊的类–>所以接口里也可以声明内嵌类.
首先,我们要明确一个原则,接口更多的是提供一种协议,他其中不应该出现任何实现相关的代码. 但是,嵌套类可以作为接口的一部分. 接口中所有成员都是隐式public static的. 既然都是static的,它可以是static的变量,也可以是static的方法,那么为什么不可以是static的内部类呢?
1.public interface ClassInInterface {
2. void howdy();
3. class Test implements ClassInInterface{
4. @Override
5. public void howdy() {
6. System.out.println("Howdy!");
7. }
8. public static void main(String[] args) {
9. new Test().howdy();
10. }
11. }
12.}
像这个样子. 埃大爷说如果想给一个接口的所有实现提供公共的代码,可以使用这种方式. 但是我实在是理解不了…
从多层嵌套类中访问外部类的成员
一句话,一个内部类被嵌套多少层都不重要,它都可以透明的访问所有外部类的成员.
1.class MNA{
2. private void f(){
3. System.out.println("111");
4. }
5. class A{
6. private void g(){
7. System.out.println("222");
8. }
9. public class B{
10. void h(){
11. g();
12. f();
13. }
14. }
15. }
16.}
17.public class MultiNestingAccess {
18. public static void main(String[] args) {
19. MNA mna = new MNA();
20. MNA.A mnaa = mna.new A();
21. MNA.A.B mnaab = mnaa.new B();
22. mnaab.h();
23. }
24.}
内部类的继承
内部类的构造器必须要连接到指向外部类的引用,也就是那个.new
语法所以如果想要继承内部类的时候,这个指向外嵌类对象的隐式的引用必须被初始化,而在子类中不再存在可连接的默认对象. 这个时候就需要用一些特殊手段了. (这么复杂的情况已经完全超出我的认知范围了,请自行脑补看到这一节的时候,我的表情)
1.class WithInner{
2. class Inner{}
3.}
4.public class InheritInner extends WithInner.Inner{
5. //can not use
6. //public InheritInner() {}
7. public InheritInner(WithInner wi) {
8. wi.super();
9. }
10. public static void main(String[] args) {
11.
12. WithInner wi = new WithInner();
13. //must use outer class' reference to complete the construction
14. InheritInner ii = new InheritInner(wi);
15. }
16.}
注意7-9行的代码,继承一个内部类的时候,构造方法必须传递一个外部类的引用,并且使用super()语法进行初始化.
覆盖内部类
内部类有时候看上去更像是一个方法,那么有没有”重写”一说呢? 再推导一下,内部类看上去像一个成员方法,成员方法是可以被重写的,那么内部类在继承的时候,也是可以被覆盖的,是吗?! 码一发!
1.class Egg{
2. private Yolk y;
3. protected class Yolk{
4. public Yolk(){
5. System.out.println("Egg.Yolk()");
6. }
7. }
8. public Egg(){
9. System.out.println("New Egg()");
10. y=new Yolk();
11. }
12.}
13.public class BigEgg extends Egg{
14. public class Yolk{
15. public Yolk(){
16. System.out.println("BigEgg.Yolk()");
17. }
18. }
19. public static void main(String[] args) {
20. new BigEgg();
21. }
22.}/*output
23.New Egg()
24.Egg.Yolk()
25.*/
不好意思,没有被覆盖. 这是为啥呢?
这要从内部类编译的产物来说了. 一个包含内部类的类(埃大爷说的很学术范儿,叫编译单元,就是个.java的源文件)编译完之后会生成类似那么几个玩意儿. 内部类会是一个独立的class文件,但是命名上会以$
来连接外嵌类和内部类. 对于匿名内部类,$
后是一串数字.
Parcel1.class
Parcel1$Destination.class
Parcel1$Contents.class
也就是说,外嵌类和内部类编译之后其实是两个独立的实体,有着各自的命名空间. 它不存在被重写一说. 但是能不能达到一个重写的效果呢,不用犹豫,肯定能.
1.class Egg2{
2. private Yolk y=new Yolk();
3. protected class Yolk{
4. public Yolk(){
5. System.out.println("Egg2.Yolk()");
6. }
7. public void f(){
8. System.out.println("Egg2.Yolk.f()");
9. }
10. }
11. public Egg2(){
12. System.out.println("New Egg2()");
13. }
14. public void insertYolk(Yolk yy){
15. y=yy;
16. }
17. public void g(){
18. y.f();
19. }
20.
21.}
22.
23.public class BigEgg2 extends Egg2{
24. public class Yolk extends Egg2.Yolk{
25. public Yolk(){
26. System.out.println("BigEgg2.Yolk()");
27. }
28. public void f(){
29. System.out.println("BigEgg2.Yolk.f()");
30. }
31. }
32. public BigEgg2(){insertYolk(new Yolk());}
33. public static void main(String[] args) {
34. Egg2 e2=new BigEgg2();
35. e2.g();
36. }
37.}/*output
38.Egg2.Yolk()
39.New Egg2()
40.Egg2.Yolk()
41.BigEgg2.Yolk()
42.BigEgg2.Yolk.f()
43.*/
局部内部类和匿名内部类的区别
局部内部类不能有权限修饰符,因为它不是外嵌类的一部分. 但是它可以访问当前代码块的常量还有外嵌类的所有成员.
1.interface Counter{
2. int next();
3.}
4.public class LocalInnerClass {
5. private int count=0;
6.
7. Counter getCounter(String name){
8. class LocalCounter implements Counter{
9. public LocalCounter(){
10. System.out.println("LocalCounter()");
11. }
12. @Override
13. public int next(){
14. System.out.print(name+" ");
15. return count++;
16. }
17. }
18. return new LocalCounter();
19. }
20.
21. Counter getCounter2(String name){
22. return new Counter(){
23. {
24. System.out.println("Counter()");
25. }
26. @Override
27. public int next() {
28. System.out.print(name+" ");
29. return count++;
30. }
31. };
32. }
33. public static void main(String[] args) {
34. LocalInnerClass lic = new LocalInnerClass();
35. Counter c1 = lic.getCounter("local innner");
36. Counter c2 = lic.getCounter2("Anonymous inner");
37.
38. for (int i =0;i<5;i++){
39. System.out.println(c1.next());
40. }
41. for(int i =0;i<5;i++){
42. System.out.println(c2.next());
43. }
44. }
45.}/*output
46.LocalCounter()
47.Counter()
48.local innner 0
49.local innner 1
50.local innner 2
51.local innner 3
52.local innner 4
53.Anonymous inner 5
54.Anonymous inner 6
55.Anonymous inner 7
56.Anonymous inner 8
57.Anonymous inner 9
58.*/
从代码上看,这一段代码匿名内部类和局部内部类实现了相同的功能. 那么他们的区别在哪里?何时用匿名内部类何时用局部内部类? 如果我们需要用一个命名构造器,或者需要重载构造器的时候,使用局部内部类,因为匿名内部类只能完成实例的初始化.