Java编程思想笔记二

Contents

  1. 1. 访问权限控制
    1. 1.1. 包:库单元
    2. 1.2. 类成员访问权限
    3. 1.3. 类的访问权限
  2. 2. 复用类
    1. 2.1. 继承
    2. 2.2. 初始化基类
    3. 2.3. 代理
    4. 2.4. 确保正确清理
    5. 2.5. 名称屏蔽
    6. 2.6. 在组合和继承之间选择
    7. 2.7. 向上转型
    8. 2.8. final关键字
    9. 2.9. 初始化及类的加载
  3. 3. 多态
    1. 3.1. 方法调用绑定
    2. 3.2. 可扩展性
    3. 3.3. 缺陷
    4. 3.4. 清理
    5. 3.5. 构造器内部的多态方法的行为
    6. 3.6. 协变返回类型
    7. 3.7. 用继承进行设计
    8. 3.8. 纯继承与扩展
  4. 4. 接口
    1. 4.1. 抽象类
    2. 4.2. 接口
    3. 4.3. 完全解耦
    4. 4.4. 多重继承
    5. 4.5. 通过继承扩展接口
    6. 4.6. 适配接口
    7. 4.7. 接口中的域
    8. 4.8. 嵌套接口
    9. 4.9. 接口与工厂
  5. 5. 内部类
    1. 5.1. 创建内部类
    2. 5.2. 链接到外部类
    3. 5.3. 使用.this和.new
    4. 5.4. 内部类与向上转型
    5. 5.5. 在方法和作用域内的内部类
    6. 5.6. 匿名内部类
    7. 5.7. 嵌套类
    8. 5.8. 为什么用内部类
    9. 5.9. 闭包和回调
    10. 5.10. 内部类与控制框架
    11. 5.11. 内部类的继承
    12. 5.12. 内部类可以被覆盖吗?
    13. 5.13. 局部内部类

访问权限控制

包:库单元

一个Java源代码文件通常称为编译单元。每个编译单元只能有一个public类,否则编译器就不会接受。如果在该编译单元之中还有额外的类的话,那么在包之外的世界是无法看见这些类的。

Java包的命名规则全部使用小写字母,第一部分是创建者的反顺序的域名。

CLASSPATH:对于.class文件,需要写出.class文件存放路径,.jar则需要写清楚.jar文件的名称。

1
CLASSPATH=.;D:\java\lib;C:\doc\grade.jar

可以用import static 语句在你的系统上使用静态的方法。

用import改变行为,c中有条件编译,java中没有相同功能,可以通过导入不同版本的包,来实习那相同功能。

类成员访问权限

修饰词 本类 同一个包的类 继承类 其它类
private Yes No No No
无(默认) Yes Yes No No
protected Yes Yes Yes NO
public Yes Yes Yes Yes

类的访问权限

  1. 每个编译单元(文件)都只能有一个public类。
  2. public类的名称必须完全与含有该编译单元的文件名相匹配,包括大小写。
  3. 虽然不是很常用,但编译单元内完全不带public类也是可能的。在这种情况下可以对文件随意命名,但是这会带来阅读和维护的混乱。

类的访问权限有两种:包访问权限和public(内部类可以是protected和private)。如果不希望其它任何人对该类拥有访问权限,可以把所有构造器都指定为private,从而阻止任何人创建该类对象,但是有一个例外,就是你在该类的static成员内部可以创建(单例模式就用到了这一点)。

如果不具有package声明,都被视作该目录下默认包的一部分。然而,如果该类的某个static成员是public的话,则客户端程序员仍旧可以调用该static成员,尽管他们不能生成该类的对象。

复用类

有两种方法可以复用现有类,第一种,只需在新的类中产生现有类的对象,称为组合;第二种,按照现有类的类型来创建新类,称为继承

继承

即使一个类只具有包访问权限,其public main()仍然是可以访问的

为了继承,一般的规则是将所有数据成员都指定为private,将所有的方法指定为public,当然特殊情况应做出调整。

初始化基类

当创建了一个导出类的对象时,该对象包含了一个基类的子对象

Java会自动在导出类的构造器中插入对基类构造器的调用,可以显示地调用基类构造器,但是调用基类构造器必须是你在导出类构造器中要做的第一件事。

代理

将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。

确保正确清理

许多情况下,清理并不是问题,交给垃圾回收器就好了。但有时有些资源必须亲自去清理,如果必须这样,最好是编写自己的清理方法,但不要使用finalize()。执行清理的顺序:首先,执行类的所有特定的清理动作,其顺序同生成顺序相反(通常这就要求基类元素仍旧存货);然后,调用基类的清理方法。

名称屏蔽

如果想要覆写一个方法,添加@Override注解,可以防止在不想重载时而意外地进行了重载,这种情况下会有错误信息。

在组合和继承之间选择

组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承则是隐式地做。

组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情况(has-a关系)。在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某个特殊需要而将其特殊化(is-a关系)。

一个最清晰的判断方法就是问一问自己是否需要从新类向基类进行向上转型。一般优先考虑组合,只在确实必要时才使用继承。

向上转型

向上转型总是很安全的。

final关键字

  1. final数据

    使用场景:一个永远不改变的编译时常量;一个在运行时被初始化的值,而你不希望它被改变。

    对于编译常量这种情况,可以在编译时执行计算式,减轻了运行时的负担。这类常量必须是基本数据类型,在这个常量定义的时候必须对其进行赋值,也可以在构造器中初始化。

    对于基本类型,final使数值恒定不变;而对于对象引用,final使引用恒定不变,一旦引用被初始化指向一个对象,就无法再把它指向另一个对象。对象自身却是可以被修改的,这一限制也适用于数组,因为它也是引用。

    按照惯例,既是static又是final的域将用大写表示,并使用下划线分隔各个单词。final 字段同时有static修饰,必须在声明时就赋值,不能在构造器中才初始化,没有static的话也可以在构造器中初始化的

    不能因为某数据是final的就认为在编译时就可以知道它的值。在运行时使用随机生成的数值来初始化final变量就说明这一点。

    final参数

    这意味着你无法在方法中更改参数引用所指向的对象,这一特性主要用来向匿名内部类传递数据。

  2. final方法

    第一个原因是把方法锁定,以防止任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。

    第二个原因是效率。在Java SE5以上应该让编译器和JVM考虑效率问题,只有出于防止覆盖时,使用final。

    所有private方法都隐式指定为final的。由于无法取用private方法,因此也就无法覆盖它。

    但是当你试图覆盖一个private方法时似乎是有效的,“覆盖”,只有在某方法是某类接口的一部分时才会出现。如果某方法是private,它就不是基类的接口的一部分。它仅是一些隐藏于类中的程序代码,只不是具有相同名称而已。但如果导出类以相同名称生成一个public、protected或包访问权限方法的话,此时你并没有覆盖该方法,仅是生成了一个新方法。

  3. final类

    final类不能被继承。不论类是否为final的,相同的规则都适用于定义为final的域。然而,final类中所有方法都隐式指定为final的,因为无法覆盖它们

初始化及类的加载

类的代码在初次使用时才加载。这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载。(构造器也是static的,类是在任何static成员被访问时被加载的)。所有的static对象和static代码段都会在加载时依定义类时的书写顺序而依次初始化的。

  1. 加载类的基类,如果基类还有基类,则继续加载,如此类推。其它任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
  2. 根基类中的static初始化(static域和static块初始化),然后下个导出类,以此类推。
  3. 根基类初始化:非static显示初始化,调用构造器。
  4. 导出类初始化,流程与基类类似。

static块可以访问、修改在它之前的static 字段,只能修改出现在它后面的static字段

多态

面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特性。

多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能创建可扩展的程序。

“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。而多态的作用则是消除类型之间的耦合关系。

方法调用绑定

将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定,叫做前期绑定。后期绑定,在运行时根据对象的类型进行绑定,后期绑定也叫做动态绑定或运行时绑定。编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体。

Java中除了static方法和final方法(private方法属于final方法)之外,其它所有的方法都是后期绑定。

可扩展性

在一个设计良好的OOP程序中,大多数或者所有方法都会只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类继承出新的数据类型,从而新添一些功能。多态是一项让程序员“将改变的事物和未变的事物分离开来”的重要技术。

缺陷

  1. “覆盖”私有方法

    只有非private方法才可以被覆盖;但是还需要密切注意覆盖private方法的现象,这时虽然编译器不会报错,但是也不会按照我们所期望的来执行。确切地说,在导出类中,对于基类中的private方法,最好采用不同的名字。

  2. 域和静态方法

    只有普通方法调用是多态的。

    任何域访问操作都在编译期进行解析,因此不是多态的(如果通常将所有域设置成为private;将基类和导出类的域赋予不同名称。这种混淆是可以避免的)。如果某个方法是静态的,它的行为就不具有多态性。

清理

第七章已经讲过了。然而,如果成员对象中存在于其它一个或多个对象共享的情况,问题就复杂了。这种情况下,也许就必须使用引用计数来跟踪仍旧访问着共享对象的对象数量了,当共享它的对象全部dispose,引用计数为0时,才会dispose。

构造器内部的多态方法的行为

构造器调用的层次结构带来了一个有趣的两难问题。如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法,那会发生什么情况呢?看下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Glyph{
void draw(){System.out.println("Glyph.draw()");}
Glyph(){
draw();
}
}

class RoundGlyph extends Glyph{
private int radius = 1;
Roundglyph(int r){
radius = r;
System.out.println("Roundglyph.Roundglyph(),radius=" + radius);
}
void draw(){
System.out.println("Roundglyph.draw(),radius=" + radius);
}
}
public class PolyConstructors{
public static void main(String[] args){
new RoundGlyph(5);
}
}/* output:
Roundglyph.draw(),radius=0
Roundglyph.RoundGlyph(),radius=5
*/

基类Glyph的构造器调用draw()方法时,radius还没有被初始化,不是1,而是0。这可能导致程序无法正常运转。

编写构造器时有一条有效的准则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其它方法”。构造器中唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法,它们自动属于final方法)

协变返回类型

Java SE5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型

1
2
3
4
5
6
7
8
9
10
class Grain{
}
class Wheat extends Grain{
}
class Mill{
Grain process(){return new Grain();}
}
class WheatMill extends Mill{
Wheat process(){return new Wheat();}
}

用继承进行设计

组合更加灵活,因为它可以动态选择类型(因此也就选择了行为);相反,继承在编译时就需要知道确切的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Actor{
public void act(){}
}
class HappyActor extends Actor{
public void act(){}
}
class SadActor extends Actor{
public void act(){}
}
class Stage{
private Actor actor = new Happyactor();
public void change(){actor=new SadActor();}
public void performPlay(){actor.act();}
}
public class Transmogrify{
public static void main(String[] args){
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
}

上述代码用到了状态模式,可以在运行时将引用与不同的对象重新绑定在一起,然后产生不同的行为。

一条通用的准则是:“用继承表达行为间的差异,并用字段表达状态上的变化”

纯继承与扩展

在导论6.中已经讲过。

Java会在运行期间对类型进行检查,这种行为称作“运行时类型识别”(RTTI)

接口

接口和内部类为我们提供了一种将接口和实现分离的更加结构化的方法。

抽象类

抽象类是接口和普通类与接口之间的一种中庸之道。

  1. 如果一个类包含一个或多个抽象方法,则必须被限定为抽象类。(反过来,如果一个类被定义为abstract的,不一定要有抽象方法)
  2. 不能实例化抽象类。
  3. 继承抽象类,如果没有实现所有的抽象方法,那么还是抽象类。

接口

interface产生一个完全抽象的类,允许创建者确定方法名、参数列表和返回类型。但只提供形式,不提供实现。(Java SE8提供了default关键字,可以定义默认的方法,该方法提供实现)。

接口可以有域,但这些域被隐式的声明为static 和final

可以在接口中显示的把方法声明为public,默认就是public,而且必须是public,方法不能是static和final的

完全解耦

创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要执行的算法中固定不变的部分,而“策略”包含变化的部分。策略就是传递进去的参数对象,包含要执行的代码,可以根据传递的参数的对象产生不同的行为。

复用代码的第一种方式就是客户端程序员遵循该接口来编写他们的类;
第二种方式:如果无法修改使用的类,则可以使用适配器设计模式

多重继承

接口可以实现多重继承,implements可以接多个接口,用逗号分开即可。

接口使用的核心原因:能向上转型为多个基类型(以及由此带来的灵活性);与抽象基类相同,防止客户端程序员创建该类的对象。

应该使用接口还是抽象类:如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,如果知道某事物应该成为一个基类,那么第一选择应该是使它成为一个接口。

通过继承扩展接口

可以通过继承来扩展接口,并且可以多重继承。只能将extends用于单一类,但是可以引用多个基类接口。

避免不同的接口中使用相同的方法。

适配接口

让方法接受接口类型,是一种让任何类都可以对该方法进行适配的方式。这就是使用接口而不是类的强大之处。

接口中的域

放入接口中的任何域都是static和final的,所以接口就成为了一种很便捷的创建常量组的工具(Java SE5出现了enum)。接口中的域是public的

嵌套接口

接口中成员默认就是public的,接口嵌套在接口中只能是public的。类中嵌套接口或类可以是4种访问权限。

当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,private接口不能在定义它的类之外被发现,返回private接口仅可以被定义它的类的方法使用。

接口与工厂

生成遵循某个接口的对象的典型方式就是工厂方法设计模式。通过工厂方法模式,我们的代码将完全与接口的实现分离,这就使得我们可以透明地将某个实现替换为另一个实现。

优先选择类而不是接口,从类开始,如果接口的需要性变得非常明确,那么就进行重构。接口是一种重要的工具,但他们容易被滥用。

内部类

内部类主要有四种:静态内部类,普通内部类,局部内部类,匿名内部类。内部类不能和外部类同名。

静态内部类不依赖于外部类实例而创建,不能外部类的普通成员变量和方法,只能访问静态成员变量和方法。
普通内部类和外部类实例绑定,可以访问外部类的任一成员和方法,不能定义静态成员和方法,但是可以有static final 成员变量。
局部内部类和局部变量一样,不能被public、protected、private、static修饰,只能访问定义为final类型的局部变量,也可以访问外部类成员。

创建内部类

典型用法:外部类通过方法返回内部类的引用。

想从外部类的非静态方法之外的任意位置创建某个内部类的对象,就得用OuterClassName.InnerClassName的形式定义内部类对象。

注意,非static内部类只能在外部类的非静态方法中直接生成对象;在外部类静态方法和其它类中,必须先生成外部类对象实例,然后外部类对象.new。

链接到外部类

内部类对象有一个到创建它的外部类对象的链接,因而可以直接的、没有任何限制地访问该外部类对象的成员,而且内部类可以访问外部类的所有成员(包括private)(C++的嵌套类没有这个特性);而外部类访问内部类的成员,必须创建内部类的对象,可以访问任何成员(包括private)。

内部类对象中隐式包含了一个外部类对象的引用。内部类对象构建需要外部类对象的引用,如果没有,编译报错(对于非静态内部类,静态内部类可以直接被创建new Outer.Inner(),而不需要先创建外部类对象)。

使用.this和.new

.this用来返回外部类引用,编译期可知道和检查正确类型,无运行时开销(OuterClassName.this);.new用来由外部类对象创建其内部类的对象,outClassObject.new InnerClassName ()(注意:在拥有外部类对象之前是不能创建内部类对象的)

嵌套类,static 内部类,其对象创建不需要外部类对象引用,也可在static方法中创建。

内部类与向上转型

内部类在向上转型的时候很有用处。因为内部类通常为某个接口的实现,且能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,能更好得隐藏细节。

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

可以在一个方法里或者任意的作用域内定义内部类,称为局部内部类

两个理由:

  1. 实现一个接口
  2. 需要一个不公开的类辅助解决复杂的问题

内部类形式

  1. 方法内的内部类;
  2. 方法中的一个作用域内的内部类;
  3. 实现接口的匿名内部类;
  4. 继承的匿名内部类(基类含有带参数的构造器);
  5. 进行字段初始化的匿名内部类;
  6. 使用实例初始化块进行构造的匿名内部类(匿名类没有构造器)。

匿名内部类

匿名内部类 :new T(){…}; {…}为匿名内部类的定义,”;”不可少(;只是该语句的结束,而不是用来表示匿名内部类的结束,所以没有什么特殊的地方)。

前面是基类构造器为默认构造器的情况,当基类构造器有参数时:new T(args){…};此时会调用基类相应构造器。

当需要用到外部定义的对象时,传递的引用参数必须为final,否则编译报错;

匿名类没有命名的构造器(类本身就没有名字),可以通过实例初始化来完成构造器的功能。

匿名内部类只能在继承类和实现接口中2选一,且只能实现一个接口。

嵌套类

static 内部类。有点类似C++嵌套类的概念,但Java的嵌套类可以访问外部类的所有成员(包括private,当然只能通过外部类对象访问非静态成员)。

  1. 不需要通过外部类对象来创建嵌套类对象;
  2. 不能通过嵌套类对象访问非静态外部类对象(嵌套类是静态的,没有外部类对象的引用,.this不可用);

普通内部类的字段和方法,只能在类的外部层次上,不能有static成员、方法和嵌套类(普通内部类有static final字段,普通内部类继承的父类中有静态域,这时可以通过内部类直接访问静态域)。

嵌套类可以位于接口内部,位于接口内部的类自动为public static的,而且嵌套类甚至本身就可以实现该接口,好处在于可以在嵌套类内编写该接口所有实现中都要用到的代码。

为什么用内部类

内部类的使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现“多重继承”。也就是说,内部类允许继承多个非接口类型(类或抽象类)。

如果拥有的是抽象的类或具体的类,而不是接口,那就只能使用内部类才能实现多重继承。

特性:

  1. 内部类可以有多个实例,每个实例可以拥有独立于外部类对象的不同信息;
  2. 一个外部类可以有多个内部类,每个内部类可以以不同的方式实现同一个接口或者继承同一个类;
  3. 内部类实例创建时刻并不受到外部类对象创建的限制;
  4. 用内部类不会制造”is-a”关系的混乱,每个内部类都是个实体。

闭包和回调

闭包是一种可调用的对象,它记录了来自创建它的作用域的一些信息。内部类是一种面向对象的闭包,不仅包含了外部类的信息,而且通过包含一个指向外部类对象的引用,可以操作所有成员,包括private。

回调,通过其它对象携带的信息,可以在稍后的某个时刻调用初始对象。回调的价值在于灵活性,可以在运行时决定需要调用的方法。 GUI编程将体现得更明显。

内部类与控制框架

一个应用程序框架是指一个用来解决一个特定类型问题的类或一组类。应用方法是,继承其中一个或多个类,覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,来解决特定问题,这是设计模式中模板方法的一个例子。 模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法。设计模式将不变的和变化的事情分开。在这个设计模式中,模板方法是保持不变的事物,而可覆盖的方法是变化的事物。

控制框架是用来响应事件的一类特殊的应用程序框架 。主要用来响应事件的系统称为事件驱动系统,如GUI。Java Swing就是一个控制框架。

内部类在控制框架中两个作用:

  1. 用来表示解决问题所需的各种不同的action()。

  2. 内部类可以直接访问外部类的所有成员,因而使得实现变得更灵活。

参见P209控制温室的运作例子。

内部类的继承

内部类指向外部类对象的引用必须初始化,而在它的继承类中并不存在要联接的缺省对象,必须使用特殊的语法明确指出这种关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class WithInner{
class Inner{
}
}

public class InheritInner extends WithInner.Inner {
InheritInner(WithInner wi){
wi.super();
}
public static void main(String[] args){
WithInner wi = new WithInner();
InheritInner ii = new InheritInner();
}
}

继承自内部类的类构造器不能是默认构造器,要有个外部类的引用作为参数,而且必须加上enclosingClassReference.super();语句,编译才能通过。

内部类可以被覆盖吗?

继承外部类,像重写方法一样重写内部类并不起作用,此时两个内部类只是两个独立的实体。

局部内部类

局部内部类不能有访问限定符;有访问局部final变量和外部类所有成员的权限;可以有命名的构造器;在方法外不能访问。

绝大部分情况下,可以用匿名类来替代局部内部类,除非:

  1. 需要命名的构造器,或者需要重载构造器
  2. 需要多个内部类的对象