复用类
两种实现方式:
- 组合:在新的类中产生现有类的对象。
- 继承:按照现有类的类型创建新类。
- 代理:
组合语法
public class B {
//在新的类中产生现有类的对象,B是新类,A是已有类。
private A a = new A();
}
class A{
private String a;
}
初始化
- 在定义对象的地方。意味着在构造器被调用之前被初始化。
- 类的构造器中。
- 在正要使用这个对象之前,这种被成为惰性初始化。
- 使用实例初始化。
继承语法
public class B extends A {
//extends 继承。按照现有类的类型创建新类
}
class A{
}
初始化子类
- 在构造器中调用父类构造器来执行初始化。
- java会自动在子类的构造器中插入对基类的调用。
- 构造过程从父类(基类)向子类(导出类)扩散。
public class B extends A {
B(){
System.out.println("from B");
}
public static void main(String[] args) {
B b = new B();
}
}
class A{
A(){
System.out.println("from A");
}
}
/* out
from A;由父类向子类扩散。执行顺序从先父类再子类。
from B;
*/
带参数构造器
含有默认构造器(即这些构造器不带参数),编辑器可以轻松的调用他们是因为不必考虑要传递什么要的参数的问题。但是,如果没有默认的父类构造器,或者想要调用一个带参数的父类构造器,就必须用关键字super显示地编写调用父类构造器的语句,并且配以适当的参数列表。
- 调用父类构造器必须是在子类构造器中要做的第一件事
public class B extends A {
B(){
super(1);//利用关键字显示地编写调用积累构造器语句。并且是放在第一步。
}
public static void main(String[] args) {
B b = new B();
}
}
class A{
A(int i){
}
}
代理
第三种方式为代理,java没有提供对它的直接支持。这是继承与组合之间的中庸之道。
我们将一个成员对象置于构造的类中(就像组合),但于此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。
- 使用代理时可以拥有更多的控制力,我们可以选择只提供在成员对象中的方法的某个子集
public class B{
private A a;
void a(){ a.a();}
void b(){ a.b();}
//我们可以通过代理不提供调用c()的方法
}
class A{
void a(){}
void b(){}
void c(){}
}
结合使用组合和继承
- 在父类没有默认构造器的时候,编译器会强制让初始化父类。但它并不会监督你必须把成员对象也初始化
public class B extends A {
private C c;
B(int i) { //在父类没有默认构造器的时候,编译器会强制让初始化父类。但它并不会监督你必须把成员对象也初始化
super(i);
}
}
class A{
public String s;
A(int i){
}
}
class C{
}
确保正确清理
如果要人为的回收对象,把自己编写的清理方法放在finally{ x } 中。
- 执行类的所有清理动作,其顺序同生成顺序相反
- 调用父类的清理方法
public class X extends A{
private B b;
private C c;
X(){
b = new B();
c = new C();
}
public static void main(String[] args) {
X x = new X();
x.dispose();
}
@Override
void dispose() {
try {
}finally {
c.dispose();//清理动作,其顺序同生成顺序相反
b.dispose();
super.dispose();
}
}
}
class A{
void dispose(){
}
}
class B extends A{
@Override
void dispose() {
super.dispose();
}
}
class C extends A{
void dispose(){
super.dispose();
}
}
名称屏蔽
如果Java的父类拥有已经被多次重载的方法名称,那么在子类中重新定义该方法名称并不会屏蔽其在父类中的任何一个版本(在C++中如要这么做需要屏蔽父类的方法)。
- @Override:重写,使用该标签在想要重写某个方法时,却重载了该方法,将会提示错误。
public class X extends A{
void dispose(String s) { //重载父类方法不需要屏蔽父类中方法
System.out.println("来自子类");
}
@Override //重写。用@Override,当你想重写某个方法,而不留心重载并非重写该方法时,编译器会报错
void dispose(int i) {
super.dispose(i);
}
public static void main(String[] args) {
X x = new X();
x.dispose("s");
x.dispose(1);
}
}
class A{
void dispose(int i){
System.out.println("来自父类");
}
}
在组合和继承之间选择
- 组合: 通常用于在新类中实现现有类的功能而非它的接口这种形式。即,在新类中嵌入摸个对象,让其实现所需功能,但新类的用户只能看到新类所定义的接口。(has-a)
- 继承: 使用现有的类,并开发一个它的特殊版本。即,你在使用一个通用类,并为了某种特殊需要将其特殊化。(is-a)
protected关键字
- 作用: 就类用户而言,这是private的,但对于任何继承次类的子类或其他任何位于同一个包内的类来说,它是可以访问的。
- 尽管可以创建protected域,但最好的方式还是将域保持为private;应到一直保留“更改底层实现”的权利,用protected方法来控制类的继承者访问权限。
public class B extends A{
public static void main(String[] args) {
B b = new B();
b.setName("name");
}
}
class A{
private String name;//保持域为private
//提供更改的权利
protected void setName(String name) {
this.name = name;
}
}
向上转型
将子类引用转换为父类引用的操作,称为向上转型
- 子类是父类的一个超集,它可能比父类含有更多的方法,但至少具备父类的所有方法,在向上转型过程中类接口唯一可能发生的事情就是丢失方法。 与之相对的还有向下转型。
再谈组合与继承
在面向对象编程中,使用组合来开发新类是最常用的。尽管OOP会多次强调继承,但还是要慎用这一技术。到底该用组合还是继承,一个最清晰的判断方法 是否需要从新类上父类进行向上转型
final关键字
根据上下文环境,Java的关键字final的含义存在着细微的区别,但通常它指的是这是无法改变的
final数据
用处:
- 一个永不改变的编译时常量
- 一个在运行时被初始化的值,而你不希望他被改变
注意:
- 一个既是static又是final的域只占据一段不能改变的存储空间
- 对于基本类型,final使数值恒定不变;而用于对象引用,final使引用恒定不变,既,一旦引用初始化指向一个对象,就无法把它改为指向另一个对象,而,对象自身却可以修改。
- public static final X_FUN:更加典型的对常量定义方式。 public 可以用被用于包之外。 static 强调只有一份。 final 说明是常量。
- 不能因为数据是final就认为编译时可以知道它的值,随机数只有运行时才确定
- 数组只是另一种引用
public class FinalData {
private static Random rand = new Random(47);
private String id;
FinalData(String id){
this.id = id;
}
private final int valueOne = 9; // 这个与下面两个都可以用作编译期常量,没有太大区别
private static final int VALUE_TWO = 99; // 常量命名全部大写,字与字之间用下划线分开。
public static final int VALUE_THREE = 39; // 更加典型的对常量定义方式。 public 可以用被用于包之外。 static 强调只有一份。 final 说明是常量。
private final int i4 = rand.nextInt(20); // final 说明只是常量。不能因为数据是final就认为编译时可以知道它的值,随机数只有运行时才确定
static final int INT_5 = rand.nextInt(20); // static 强调内存中只有一份。
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private final int[] a = {1,2,3,4,5,6};
@Override
public String toString() {
return id + "; " + " i4 =" + i4 + " , INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; // Error 对于基本类型 final 使数值恒定不变
//! fd1.v2 = new Value(3); // Error 对于对象引用 final 使引用恒定不变
fd1.v1 = new Value(2); // 不是 final 就可以重新引用
fd1.v2.i++; // 但可以对象本身是可以修改的。不能因为是final就认为值无法改变。
//! fd1.a = new int[34]; // 数组和对象相同。引用不能改变
fd1.a[1]++; // 但对象本身可以修改。
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1); // fd1; i4 =15 , INT_5 = 18
System.out.println(fd2); // fd2; i4 =13 , INT_5 = 18
// i4值不同,不同实例初始化会有不同的值
// 但 INT_5 不能通过创建第二个对象改变。因为它是static,在装载时已被初始化。且只占据一份内存。
}
}
class Value{
int i;
Value(int i){
this.i = i;
}
}
空白final
Java允许生成“空白final”,所谓空白final就是指被声明为final但又未给定初值的域。无论什么情况,编译器都必须确保空白final在使用前被初始化。空白final在关键字final的使用上提供了更大的灵活性,为此一个类中的final域就可以根据对象而有所不同。
public class BlankFinal {
private final int i = 0;// 初始化的final
private final int j ; // 空白final
private final Poppet p; //空白引用
public BlankFinal() {
//必须在域的定义处或每个构造器中用表达式对final进行赋值,使得final域在使用前总是被初始化
this.j = 1;
this.p = new Poppet(1);
}
public BlankFinal(int x){
j = x;
p = new Poppet(x);
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(1);
}
}
class Poppet{
private int i;
Poppet(int ii){
i = ii;
}
}
final参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象。
public class FinalArguments {
void with(final int i,int j){
//! i = 1; //Error
//! i++; //Error
j++; //OK
}
}
- 你可以读参数,但却无法参数。
- 这一特性主要用来向匿名内部类传递数据
final方法
使用final方法的原因有两个:
- 把方法锁定,以防任何继承类修改它的含义。
另一原因是效率。但现在已经不用了。
因此只有在想要明确禁止覆盖时,才讲方法设置为final。
final和private关键字
类中所有的private方法都地指定为是final。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但并不能给该方法增加任何额外的意义。
这一问题会造成混淆。因为,如果你试图覆盖一个private方法(隐含是final),似乎是奏效的,而且编译器给出错误信息。
class WithFinals{
private final void f(){
System.out.println("WithFinals f()");
}
private void g(){
System.out.println("WithFinals g()");
}
}
class OverridingPrivate extends WithFinals{
private final void f(){
System.out.println("OverridingPrivate f()");
}
private void g(){
System.out.println("OverridingPrivate g()");
}
}
class OverridingPrivate2 extends OverridingPrivate{
public final void f(){
System.out.println("OverridingPrivate2 f()");
}
public void g(){
System.out.println("OverridingPrivate2 g()");
}
}
public class FinalOverridingIllusion {
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
//你可以向上转型
OverridingPrivate op = op2;
//! op.f(); //但你调用不了父类方法
//! op.g();
WithFinals wf = op;
//! wf.f();
//! wf.g();
}
}
“覆盖”只有在某方法是基类的接口的一部分才会出现。即,必须能将一个对象向上转型为它的基本类型并调用同样的方法。如果某方法为private,它就不是父类的接口的一部分。它仅是一些隐藏与类中的代码,只不过是具有相同的名称而已。但如果在子类以相同的名称生成一个public、protected或包访问权限的方法的话,该方法就不会产生在父类中出现的 “仅具有相同名称” 的情况。此时并没有覆盖方法,但是生成了一个新方法。由于private方法无法触及及而且能有效隐藏,所以除了把它堪称是因为它所归属的类的组织结构原因而存在以外,其他任何事物都不要考虑到它。
(简而言之:private方法不是类的接口的一部分,子类不能覆盖,也调用不了。)
final类
当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,你对该类的设计永不需要做任何变动,或者出于安全的考虑,你不希望它有子类
final class Dinosaur{
}
//! class Further extends Dinosaur{} //不能继承自final类
- final的域可以根据个人选择为是或不是final。无论类是否被定义为final。相同的规则都适用于定义为final的域。 (因为域都是final的话,那就说明所有值都无法改变嘛)
- 由于final类禁止继承,所有final中所有的方法都是隐式制定为final的。
有关final的忠告
- 设计类时,将方法指明是final,应该说是明智。你可能会觉得,没人会想要覆盖你的方法。有时这是对的。但要遇见类是如何被复用一般是很困难的,特别是对于一个通用来更是如此。如果将一个方法指定为final,可能会妨碍其他程序员在项目中通过继承来复用你的类,而这只是因为你没有想到它会以那种方式被运用。(简而言之:别人可能会想到比你更多的用法,但却被final限制住)
初始化及类的装载
在许多传统语言中,程序作为启动过程的一部分立刻被加载的。然后是初始化,接着程序运行。这些语言的初始化过程必须小心控制,以确保定义为static的东西,其初始化的顺序不会造成麻烦。列如C++中,如果某个static期望另一个static在被初始化之前就能有效的使用它,就会出现问题。
Java中不会出现这种问题,因为它采用了一种不同的加载方式。加载是众多变得更加容易的动作之一,因为Java中所有事物都是对象。每个类的编译代码都存在自己的独立的文件中。该文件只有在需要程序代码时才会被加载。既 类的代码在初次使用时才会被加载 。通常是指:加载发生于创建类的第一个对象之时,但访问static方法时,也会发生加载。
- 构造器也是static方法,尽管没有显示的写出来。因此更准确的将,类是在其任何static成员被访问时加载的
继承与初始化
class Insect{
private int i = 9;
protected int j;
Insect(){
System.out.println("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 = printInit("static Insect.x1 init");
static int printInit(String s){
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect{
private int k = printInit("Beetle.k init");
Beetle(){
System.out.println("k = " + k);
System.out.println("j = " + j);
}
private static int x2 = printInit("static Beetle.x2 init");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle b = new Beetle();
}
}
/*out
static Insect.x1 init
static Beetle.x2 init
Beetle constructor
i = 9, j = 0
Beetle.k init
k = 47
j = 39
*/
- 运行时,第一件事就是访问Beetle.main(static方法,程序默认首先调用)。加载器开始启动并找出Beetle的类的编译代码。
- 加载过程中,编译器注意到它有一个父类,于是它继续加载。不管你是否打算产生一个基类对象,这都会发生
- 如果自身还有父类,则会继续加载,如此类推。接来下,父类中的static初始化则会被执行,然后是下一个子类。
- 所有的的类都加载完毕,对象就可以创建
- 对象中所有的基本类型都会设为默认值,对象引用设为null。
- 然后,父类构造器被调用。父类构造器和子类的构造器一样,以相同的顺序经历相同的过程
- 父类构造器完成之后,实例变量按起次序被初始化。
- 最后,构造器的其余部分被执行。
JAVA类首次装入时,会对静态成员变量或方法进行一次初始化,但方法不被调用是不会执行的,静态成员变量和静态初始化块级别相同,非静态成员变量和非静态初始化块级别相同。
初始化顺序:先初始化父类的静态代码—>初始化子类的静态代码–>
(创建实例时,如果不创建实例,则后面的不执行)初始化父类的非静态代码(变量定义等)—>初始化父类构造函数—>初始化子类非静态代码(变量定义等)—>初始化子类构造函数
总结
- 继承和组合都能从现有的类型产生新的类型。组合一般是将现有的类型作为新类型底层实现的一部分加以复用,而继承复用的是接口。
- 使用继承时,由于子类具有父类的接口,因此可以向上转型为父类,这对多态来讲至关重要。
- 尽管面向对象变成对继承极力强调,但在开始一个设计时,一般优先选择使用组合(或代理),只有确实必要时才使用继承。因为组合更具灵活性。此外,通过对成员类型使用继承技术的添加技巧,可以在运行时改变那些成员对象的类型和行为。因此,可以在运行时改变组合而成的对象的行为。
- 在设计一个系统时,目标应该是找到或创建某些类,其中每个类都有具体的用户,而且既不会太大,也不会太小。如果设计的过于复杂,可以将现有类拆分为更小的部分而添加更多的对象。