第8章:接口与抽象类(深入多态)
抽象类
用abstract关键字声明抽象类,抽象类不能用new 关键字进行实例化。在设计继承结构时,必须决定清楚什么类是抽象类,什么类是具体类。编译器不会让你初始化一个抽象类。抽象类,除了被继承以外,是没有其它任何用途的。抽象类中,必须包含有抽象方法,还可以包含非抽象方法。
抽象方法
即用 abstract关键字声明的方法,抽象方法没有方法实体,即没有具体的实现过程。拥有抽象方法的类,必须声明为抽象类。抽象类中的抽象方法,用于规定一组子类共同的协议。
abstract class Animal {
// 抽象方法,没有方法体
public abstract void eat();
}
在继承过程中,具体类必须实现抽象父类的所有抽象方法
抽象方法没有具体的方法体,它只是为了标记出多态而存在。在覆写抽象父类的抽象方法时,方法名、参数列表必须相同,返回值类型必须兼容。Java很在乎你是否实现了抽象类的抽象方法。
public class Canine extends Animal {
// 覆写抽象类的抽象方法
public void eat() {
System.out.println("Canine,会吃食物!!");
}
// 非继承的方法
public void roam() {
}
}
多态的使用
在Java中,所有类都是从Object这个类继承而来的,Object是所有类的源头,它是所有类的父类。Object有很有用的方法,如 equals(), getClass(), hashCode(), toString()等。
Object类,是抽象类吗? 答:不是,它没有抽象方法。
是否可以覆写Object中的方法? 答:Object类中带有 final关键字的方法,不能被覆写。
Object类有什么用? 答:用途一,它作为多态可以让方法应付多种类型的机制,以及提供Java在执行期对任何对象都需要的方法实现。另一个用途,它提供了一部分用于线程的方法。
既然多态类型这么有用,为什么不把所有的参数类型、返回值类型都设定为Object? 答:因为Java是强类型语言,编译器会检查你调用的是否是该对象确实可以响应的方法。即,你只能从确实有该方法的类中去调用。
Object dog = new Dog();
dog.toString(); // 这可以通过编译,因为toString()是Object类中自有的方法。
dog.eat(); // 这将无法通过编译,因为dog是Object类型,它调用的eat()方法在Object类中没有。
在使用多态时,要注意对象多种类型之间的差异。如下代码:
Dog dog1 = new Dog();
Animal dog2 = new Dog();
Object dog3 = new Dog();
注意这三个dog对象的区别: dog1 拥有 Dog / Animal / Object中所有的方法。dog2 拥有 Animal / Object 中的方法,不能调用 Dog 类特有的方法。 dog3 只拥有Object 中的方法,不能调用 Animal / Dog类中的方法。这就是在使用多态过程中,需要特别注意的问题。
那么该如何把 Object 类型的 dog转化成真正的 Dog 类型呢?
if (dog2 instanceof Dog) {
Dog dog4 = (Dog)dog2;
}
if (dog3 instanceof Dog) {
Dog dog5 = (Dog)dog3;
}
// 此时,dog4 / dog5 就是真正的 Dog类型了。
接口
接口,是一种100%纯抽象的类。接口中的所有方法,都是未实现的抽象方法。
接口的作用
接口存在的意义,就是为了解决Java多重继承带来的致命方块问题。为什么接口可以解决致命方块的问题呢?因为在接口中,所有方法都是抽象的,如此一来,子类在实现接口时就必须实现这些抽象方法,因此Java虚拟机在执行期间就不会搞不清楚要用哪一个继承版本了。
// interface关键字,用于定义接口
public interface Pet {
public abstract void beFriendly();
public abstract void play();
}
// 继承抽象父类 Animal类, 实现 Pet接口
public class Dog extends Animal implements Pet {
// 实现接口中的抽象方法
public void beFriendly() {
System.out.println("实现 Pet接口中的 beFriendly()方法");
}
// 实现接口中的抽象方法
public void play() {
System.out.println("实现 Pet接口中的 play()方法");
}
// 覆写抽象父类中的抽象方法
public void eat() {
System.out.println("覆写抽象父类中的eat()抽象方法");
}
}
同一个类,可以实现多个接口!
public class Dog extends Animal implements Pet, Saveable, Paintable { ... }
super关键字
super代表父类,在子类中使用 super关键字指代父类,通过super还可以调用父类的方法。
// 抽象父类
abstract class Animal {
void run () {}
}
// 继承父类
class Dog extends Animal {
void run () {
super.run(); // 这里,调用并执行父类的 run() 方法
// do other things
}
}
Dog d = new Dog();
d.run(); // 这调用的是子类Dog对象的 run()方法。
第9章:构造器与垃圾收集器
堆(heap)、栈(stack)
当Java虚拟机启动时,它会从底层操作系统中取得一块内存,以此区段来执行Java程序。实例变量保存在所属的对象中,位于堆上。如果实例变量是对象引用,则这个引用和对象都是在堆上。
构造函数与对象创建的三个步骤
对象创建的三个步骤:声明、创建、赋值。
构造函数,让你有机会介入 new 的过程。构造函数,没有显示的指定返回值类型,构造函数不会被继承。如果一个类,没有显示地编写构造器函数,Java编译器会默认地为该类添加一个没有参数的构造器函数。反之,Java编译器则不会再添加任何默认的构造函数。
Dog dog = new Dog();
构造器函数重载
即一个类,有多个构造器函数,且它们的参数都不能相同,包括参数顺序不同、或者参数类型不同、或者参数个数不同。重载的构造器,代表了该类在创建对象时可以有多种不同的方式。
public class Mushroom {
// 以下五个构造器,都是合法的,即构造器重载
public Mushroom() {}
public Mushroom( int size ) {}
public Mushroom( boolean isMagic ) {}
public Mushroom( boolean isMagic, int size ) {}
public Mushroom( int size, boolean isMagic ) {}
}
构造函数链 super()
构造函数在执行的时候,第一件事就是去执行它的父类的构造函数。这样的链式过程,就被称为“构造函数链(Constructor Chaining)”。
class Animal {
public Animal() {
System.out.println("Making an Animal");
}
}
class Dog extends Animal {
public Dog() {
super(); // 如果没有这句,Java编译器会默认添加上这句,即调用父类的无参构造器
System.out.println("Making an Dog");
}
}
public class ChainTest {
public static void main(String[] args) {
System.out.println("Starting...");
Dog d = new Dog();
}
}
如果一个类,没有显示地书写构造器函数,Java编译器会为它添加上默认的无参构造器。如果在一个子类的构造器中没有使用super()调用父类的某个重载构造构造器,Java编译器会为这个子类的构造器默认添加上super(),即在子类的构造器函数中调用父类的无参构造器。
父类的构造器函数,必须在子类的构造器函数之前调用。在子类构造器函数中调用父类构造器时,必须与父类构造器的参数列表一致。
在类中,this 和 super 有什么区别? this() 和 super() 有什么区别?
使用 this() 可以在某个构造函数中调用同一个类的另外一个构造函数。 this() 只能在构造函数中使用,并且必须是第一行。 this() 和 super() 不能同时使用。
class Car {
private String name;
// 父类的有参构造器
public Car(String name) {
this.name = name;
System.out.println(name);
}
}
class MiniCar extends Car {
// 构造器
public MiniCar(String name) {
// 调用父类的有参构造器
super(name);
}
// 另一个构造器
public MiniCar() {
// 调用同一个类的另一个构造器
this("这里子类汽车的名称");
}
}
public class TestThis {
public static void main(String[] args) {
MiniCar mc1 = new MiniCar();
MiniCar mc2 = new MiniCar("动态的名字");
}
}
对象、变量的生命周期
对象的生命周期决定于对象引用变量的生命周期,如果引用还在,则对象也在;如果引用死了,对象会跟着被 GC 回收。当最后一个引用消失时,对象就会变成可回收的。
局部变量,只存活在对象的方法中,方法结束,局部变量就死了。
实例变量,存活在对象中,它的生命周期与对象一致。
Life 和 Scope的区别
Life,只要变量的推栈块还存在于堆栈上,局部变量就算是活着。局部变量会活到方法执行完毕为止。
Scope,局部变量的作用范围只限于它所在的方法中。当该方法调用别的方法时,该局部变量还活着,但不在目前的范围内,当被调用的其它方法执行完毕后,该总局变量的范围又跟着回来了。
第10章:数字与静态性
Math的特点
在 Java 中没有东西是全局(global)的。但,Math 方法是接近全局的方法。Math不能用来创建实例变量。因为Math是用来执行数据计算的,所以没有必要创建对象来进行数学计算,创建对象是浪费内存空间的做法。Math中所有方法都静态方法。
···java
Long a = Math.round(46.25);
int b = Math.max(2, 3);
int c = Math.abs(-500);
···
非静态方法与静态方法
静态方法,使用 static 关键字声明,以类的名称进行调用。
非静态方法,以实例对象进行调用,没有 static修饰。
Math类是如何阻止被实例化的?
Math类阻止被实例化,采取的策略是使用 private 修饰了其构造函数。
静态变量,会被同类的所有实例共享,因为它隶属于类。静态变量,在类第一次载入时初始化。静态变量,会在所有对象创建之前进行初始化,也会在任何静态方法执行之前就初始化。静态变量,只能由类来调用。
非静态变量,只被单个对象独有,它隶属于实例。非静态变量,在类实例化时进行初始化。
实例对象不会维护静态变量的拷贝,静态变量由类进行维护。非静态变量由实例对象进行维护。
class Duck {
// 非静态变量,属于对象
private int size = 0;
// 静态变量,属于类
private static int count = 0;
public Duck() {
size++;
count++;
System.out.println("size " + size);
System.out.println("count " + count);
}
public void setSize(int s) {
size = s;
}
public int getSize() {
return size;
}
}
public class TestStatic {
public static void main(String[] args) {
Duck d1 = new Duck(); // size = 1 count = 1
Duck d2 = new Duck(); // size = 1 count = 2
Duck d3 = new Duck(); // size = 1 count = 3
}
}
声明一个静态常量
public static final double PI = 3.1415926;
public 表示可供各方读取。 static 表示静态。 final 表示“固定不变”。 常量的标识符,一般建议字母大写,字母之间可以用下划线连接。
深入理解 final
final 的核心意思是,它所修饰的元素不能被改变。final 不仅可以修饰变量,还可以修饰方法和类。
// 用 final 修饰的类,不能被继承
final class Foo {
// final 修饰静态变量,得到静态常量
public static final double PI = 3.1415926;
// final 修饰非静态变量,该变量将无法再被修改
final String name = "geekxia";
void changeName() {
// 修改 final变量,失败
// name = "Evatsai";
}
// final 修饰局部变量,该局部变量也将无法再被修改
void doFoo(final int x) {
// 修改局部变量,失败
// x = 100;
}
// final 修饰的方法,子类将不能覆写
final String getName() {
return name;
}
}
主数据类型的包装类
Boolean / Character / Byte / Short / Integer / Long / Float / Double
主数据类型的包装类,都放在 java.lang 中,所以无需 import 它们。当你需要以对象的方式来处理主数据类型时,你就需要用包装类把它们包装起来,Java5.0之前必须这么做。
int a = 123;
// 包装
Integer aWrap = new Integer(a);
// 解包
int b = aWrap.intValue();
Java5.0以后,autoboxing 使得主数据类型和包装类型不必分得那么清楚。autoboxing 的功能能够自动地将主数据类型和包装类型进行转换。看看下面的例子:
ArrayList<Integer> list = new ArrayList<Integer>();
// 自动地把主数据类型和包装类型进行转换
list.add(3);
int c = list.get(0);
Boolean bool = new Boolean(null);
if (bool) {}
Integer d = new Integer(3);
d++;
Integer e = d + 3;
void takeNumber(Integer i) {}
Integer getNumber() { return 4; }
主数据类型与字符串之间的相互转化
// 把字符串转化成主数据类型
String a = "12";
int b = Integer.parseInt(a);
double c = Double.parseDouble("50.789");
boolean d = new Boolean("true").booleanValue();
// 把主数据类型转化成字符串
double e = 34.789;
String f = "" + e;
String g = Double.toString(e);
如何对数字进行格式化?
// 第二个参数,以第一个参数的格式化指令进行输出
String s = String.format("%, d", 1000000000);
String m = String.format("I have %,.2f bugs to fix.", 489369.123456);
如何对日期进行格式化?
String d = String.format("%tB", new Date());
更多有关“格式化指令”的参考,请查看相关手册。
使用 Calendar 抽象类操作日期
// 获取日历的实例对象
Calendar cal = Calendar.getInstance();
cal.set(2014, 1, 7, 15, 40);
long day1 = cal.getTimeInMillis();
day1 += 1000*60*60;
cal.setTimeInMillis(day1);
cal.add(cal.DATE, 35);
第11章:异常处理
如果你把有风险的程序代码包含在 try/catch 块中,那么编译器会放心很多。 try/catch 块会告诉编译器你确实知道所调用的方法会有风险,并且也已经准备好要处理它。
try {
// 有风险的行为
Sequencer seq = MidiSystem.getSequencer();
System.out.println("had got a sequencer");
} catch (MidiUnavailableException ex) {
System.out.println(ex);
} finally {
System.out.println("总会被执行!");
}
异常也是多态的,Exception子类的实例对象,都是 Exception类型的。编译器会忽略 RuntimeException类型的异常,RuntimeException类型的异常不需要声明或被包含在 try/catch 块中。
如果 try 或 catch 块中有 return 指令,finally还是会执行。流程会跳到 finally执行,finally执行完毕后再回跳到return 指令。
处理多重异常
class Laundry {
public void doLaundry() throws PantsException, LingerieException {}
}
public class HandleException {
public void go() {
Laundry lau = new Laundry();
try {
lau.doLaundry();
} catch (PantsException e) {
System.out.print(e);
} catch (LingerieException e) {
System.out.print(e);
} finally {
}
}
}
如果有必要的话,方法可以抛出多个异常。但该方法的声明必须要有含有全部可能的检查异常(如果两个或两个以上的异常有共同的父类时,可以只声明该父类就行)。
有多重异常需要捕获时,异常要根据继承关系从小到大排列,如果更高一级的异常排在了前面,将会导致低一级的异常将没有机会被使用。同级别的异常,排列顺序无所谓。
异常
异常是由方法 throws 来的。
public void doLaundry() throws PantsException, LingerieException {}
如果你调用了一个有风险的方法,但你又不想 try/catch捕获时怎么办?你可以继续使用 throws 关键字,把异常抛给下一个调用我的人,这好比是在踢皮球哈哈。
从上面看,程序有两种方式来处理异常,一种是使用 try/catch 来捕获异常,另一种是使用 throws 把异常抛给下一个调用者。