类和接口是Java程序设计语言的核心,也是Java语言的基本抽象单元。Java语言提供了许多强大的基本元素,供程序员用来设计类和接口。
设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之前只通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况。这个概念被称为信息隐藏或封装,是软件设计的基本原则之一。
Java程序设计语言提供了许多机制来协助信息隐藏。访问控制机制决定了类、接口和成员的可访问性。
第一条规则很简单:尽可能地使每个类或者成员不能被外界访问。
对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别:
1)私有的(private)——只有在声明该成员的顶层类内部才可以访问这个成员。
2)包级私有的(package-private)——声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为“缺省(default)访问级别”,如果没有为成员钉钉访问修饰符,那就是这个访问级别。
3)受保护的(protected)——声明该成员的类和它的子类可以访问这个成员,并且,声明该成员的包内部的任何类也可以访问这个成员。
4)公有的(public)——在任何地方都可以访问该成员。
坚持面向对象程序设计思想的看法的是正确的:如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变类的内部表示法的灵活性。
然而,如果是包级私有的,或者是私有的嵌套类,这几暴露它的数据域并没有本质的错误。
总之,公有类永远都不该暴露可变的域。
不可变类只是实例不能被修改的类。每个实例中所包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。
为了使类成为不可变,要遵循下面五条规则:
1)不要提供任何会修改对象状态的方法。
2)保证类不会被扩展。使这个类成为final的。
3)使所有的域都为final的。
4)使所有的域都成为私有的。
5)确保对于任何可变组件的互斥访问。
不可变对象本质上是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时,它们不会遭到破坏。
“不可变对象可以被自由地共享”导致的结果是:永远也不需要进行保护性拷贝。
不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
不可变对象为其他对象提供了大量的构件,无论是可变的还是不可变的对象。
不可变对象真正唯一的缺点是:对于每一个不同的值都需要一个单独的对象。
让不可变的类变成final的另一种方法就是,让类的所有构造器都变成私有的或者包级私有的,并添加共有的静态工厂来代替公有的构造器。
继承是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具。使用不当会导致软件变得很脆弱。
与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。
如果在适合于使用复合的地方使用了继承,则会不必要地暴露实现细节。这样得到的API会把你限制在原始的实现上,永远限定了类的性能。
简而言之,继承的功能非常强大,但是也存在诸多问题,因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。
对于不是为了继承而设计、并且没有文档说明的“外来”类进行子类化是很危险的。而对于专门为了继承而设计并且具有良好文档说明的类而言,该类的文档必须精确地描述覆盖每个方法所带来的影响。
好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。
为了允许继承,类必须遵守其他一些约束:构造器不能调用可被覆盖的方法,无论是直接调用还是间接调用。
package JavaDay5_28;
import java.util.Date;
/**
* @author [email protected]
* @date 18-5-28 下午7:04
*/
public class Demo1 {
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
class Super {
public Super() {
overrideMe();
}
public void overrideMe() {}
}
class Sub extends Super {
private final Date date;
public Sub() {
date = new Date();
}
@Override
public void overrideMe() {
super.overrideMe();
System.out.println(date);
}
}
对于以上栗子,可能期望输出当前日期两次,然后第一次输出为null,输出结果如下:
因为overrideMe方法被Super构造器调用时,构造器Sub还没有机会初始化data域。
Java程序设计语言提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。这两种机制最明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许。一个更为重要的区别在于,为了实现抽象类定义的类型,类必须成为抽象类的一个子类。任何一个类,只要它定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口,而不管这个类是处于类层次的哪个位置。因为Java只允许单继承,所以抽象类作为类型定义受到了极大的限制。
现有的类可以很容易被更新,以实现新的接口。
接口是定义mixin(混合类型)的理想选择。
接口允许我们构造非层次结构的类型框架。
通过包装类模式,接口使得安全地增强类的功能成为可能。
虽然接口不允许包含方法的实现。但是,通过对导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的有点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。
设计共有的接口要非常谨慎。接口一旦被公开发行,并且已经被广泛实现,再想改变这个接口几乎是不可能的。
简而言之,接口通常是定义允许多个实现的类型的最佳途径。
当类实现接口时,接口就充当可以引用这个类的实例的类型。
接口应该只被用来定义类型,不应该被用来导出常量(即常量接口)。
子类型化是面向对象语言提供的能表示多重风格的单个数据类型。以下为一个标签类的栗子:
package JavaDay5_28;
/**
* @author [email protected]
* @date 18-5-28 下午7:57
*/
public class Figure {
enum Shape { RECTANGLE, CIRCLE};
final Shape shape;
double length;
double width;
double radius;
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch (shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
为了将标签类转变成类层次,要为标签类中的每个方法都定义一个包括抽象方法的抽象类,每个方法的行为都依赖于标签值。以下为转换为类层次的代码:
package JavaDay5_28;
/**
* @author [email protected]
* @date 18-5-28 下午8:02
*/
abstract class FigureDemo {
abstract double area();
}
class Circle extends FigureDemo {
final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends FigureDemo {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
double area() {
return length * width;
}
}
简而言之,标签类很少有使用的时候,大多数标签类都可以被类层次所代替。
有些语言支持函数指针、代理、lambda表达式,或者支持蕾丝的机制,允许程序把“调用特殊函数的能力”存储起来并传递这种能力。这种机制通常用于允许函数的调用者通过传入第二个函数,来指定自己的行为。
Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象的方法通常就是执行该对象上的某项操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。
函数指针的主要用途就是实现策略模式。
嵌套类是指被定义在另一类的内部的类。嵌套类存在的目的应该只是为它的外围类提供服务。如果嵌套类将来可能回用于其他的某个环境中,他就应该是顶层类。
嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。除了静态成员类,其他三中都被成为内部类。
静态成员类是最简单的一种嵌套类。它可以访问外围类的所有成员,包括那些声明为私有的成员。当它被声明为私有的,它就只能在外围类的内部才可以被访问。静态成员类的一种常用用法就是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。
从语法上将,静态成员类和非静态成员类的唯一的区别就是P:静态成员类的声明中包含修饰符static。
如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
匿名类不同于Java程序设计语言中的其他任何语法单元。匿名类灭有名字。他不是外围类的一个成员。它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名类除了在它们被声明的时候之外,是无法被实例化的。匿名类的一种常见用法就是动态地创建函数对象。
局部类是四中嵌套类中用得最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。
总结:本章主要讨论了类和接口的相关知识,看完之后,领悟到Java是一门真正意义上的面向对象的语言,应该多使用接口或者抽象类来编程,并注意编程中关于接口编程的问题。共勉。