第四章 面向对象特性
?类和封装
?继承和多态
4.1 类和封装
? 基本结构
? Methods
? 封装
? instance variables
? 构造器和初始化
Java类的基本结构
? class class_name{
constructor1//构造器
constructor2

method1//方法
method2

instance variable1//实例变量
field2
…,
}
Method
behavior and state
一个类的每个对象可以拥有不同的实例变量,而它们的
方法行为呢?
应该说,同一类的每个实例具有同样的方法,但是这些
方法也可以具有不同的行为,取决于实例变量。
Method
Song
title
artist
setTitle()
setArtist()
play()
State
(knows)
Behavior
(does)
void play() {
soundPlayer.playSound(title);
}
所有实例的 play方法都是一样的;
play()方法的行为将因 title的不同而不
同。
Song t1=new Song();
t1.setTitle(“Travel”);
t1.play();
Song t2=new Song();
t2.setTitle(“Sing”);
t2.play();
Song,Travel” Song,Sing”
behavior and state
Method
Dog
size
name
bark()
void bark() {
if(size >60)
System.out.println(“Woof!”);
else if(size >14)
System.out.println(“Ruuf!”);
else
System.out.println(“Yip!”);
}
size不同,bark()的是不一
样的。
behavior and state
Method
pass something to method
Dog d=new Dog();
d.bark(3); d.bark(4);
void bark(int numOfBarks){
while(numOfBarks>0){
System.out.println(“ruff”);
numOfBarks--;
}
}
arguments
parameter
在方法内充当
局部变量
值 3被发送出去
Method
get things back from a method
void go(){
//return nothing
}
int t=life,giveSecret();
int giveSecret() {
return 42;
}
类型必
须匹配
这里必须是
一个 int
值 42被发送出去
每个方法必须声明返回类型,它也叫该方法的类型
Method
Java is pass-by-value
int x=7; x
void go(int z) { } z
x z
x z
2 foo.go(x); 传递的是变量的值,而不是变量的地址
3 如果 z改变,x不会受影响
void go(int z) {
z=0;
}
1
Method
Getter and Setter
Employee
salary
name
setName()
用来读取和设置属性
setSalary()
getName()
getSalary()
class Employee {
double salary;
String name;
String getName() {
return name;
}
void setName(String s){
name=s;
}
}
封装
防止实例变量的值被随意设置
theCat,height=27 通过引用可以访问对象的变量或者调用对象的方法
theCat,height=0
对象 theCat的
实例变量 height
被设置
没有任何限制的访问权限造成
height被置 0,或者为负数。
public void setHeight(int ht){
if(ht>9) {
height= ht;
}
}
通过强制的方法,让用户只
能通过 setHeight方法来设
置实例变量,可以确保任何
不合理的值被排除掉。
封装的方法
使用访问修饰符( access modifier),
public & private
尽量让实例变量 private,并提供 public的 getter&&setter,
封装
例子
class Dog {
private int size;
public int getSize() { return size;}
public void setSize(int s) { size=s;}
void bark() {
if( size>60)
System.out.println(“wwof”);
else
System.out.println(“yep”);
}
}
class DogTestDrive {
public static void main(String[] args) {
Dog d=new Dog();
d.size=40;
d.bark();
}
}
class DogTestDrive {
public static void main(String[] args) {
Dog d=new Dog();
d.setSize(40);
System.out.println(“Dog 1:,+
d.getSize());
d.bark();
}
}
instance variables
声明和初始化
对于局部变量 int size;
String name;
name type
int size=10;
String name=“DL”;
声明
初始化
System.out.println(“size is,+size);
size=10;
System.out.println(“size is” +size);
在使用变量之前必
须初始化或者变量
被赋值。
使用
instance variables
声明和初始化
对于实例变量
class PoorDog {
private int size;
public int getSize() {
return size;
}
}
public class DogTestDrive {
public static void main(String[] args){
Dog d=new Dog() ;
System.out.println(,Dog size is,
+ d.getSize() ;
}
返回的值是多少?
instance variables
声明和初始化
实例变量总会得到一个缺省的值,
integers 0
floating points 0.0
booleans false
references null
但是,局部变量是没有缺省值的,它
们必须保证先得到一个值!
构造器和初始化
object creation
声明一个引用变量
Dog myDog = new Dog();
虚拟机为该变量分配空间,并命名为 myDog。
创建一个对象
Dog myDog=new Dog();
虚拟机在堆中为新的 Dog对象分配空间
将该对象与引用连接起来。
Dog myDog=new Dog();
现在可以通过这个引用 myDog远程控制该 Dog对象
1
2
3
对象是怎样被创建的?
构造器和初始化
object creation
Dog myDog=new Dog();
类似于一个方法调用
注意这实际并不是方法,而是 调用了 Dog类的构造
器( Constructor),
当你说 new时,它启动一段特定的代码。
构造器和初始化
Every class has at least a constructor
构造器在哪儿?
---自己编写,或者
如果不提供,编译器会提供一个缺省构造器,
public Dog() {
//constructor code here
}
注意到它与 method的区别了吗?
构造器和初始化
Every class has at least a constructor
public Dog() {
//constructor code here
}
1 它没有返回类型
2 它必须和类同名
构造器和初始化
Construct a Duck
声明对象引用变量
创建对象
连接这两个变量
go to Constructor
public class Duck{
public Duck() {
System.out,
println(“Quack”);
}
public class UseDuck {
public static void
main(String[] args){
Duck d=new Duck();
}
构造器和初始化
Initialization
public class Duck {
int size;
public void setSize(int s) {
size=s;
}
}
public class DuckTest {
public static void main(String[] args) {
Duck d=new Duck() ;
d.setSize(42);
}
}
编译器插入一段缺省构造器代码。
但是在这之间,Duck的 size为 0
1 如果 自己不提供,编译器会安排一
个默认的构造器,此时,实例变量将
得到所属类型的缺省值。
这样有时是不安全的,下例中要
求使用者必须把两句话连起来写。
构造器和初始化
2 构造器的基本作用是初始化对象的状态。
Initialization
public Duck() {
size= 34;
}
设置实例变量的值
public class DuckTest {
public static void main(String[] args) {
Duck d=new Duck() ;
…,
}
}
从此处起,d的默认 size是 34
构造器和初始化
Initialization
3 如果要由用户来指定初始值,应该设计带参的构造器
public class Duck {
int size;
public Duck(int s) {
size=s;
}
}
public class DuckTest {
public static void main(String[] args) {
Duck d=new Duck(42) ;
…,
}
}
这次不需要再调用 setSize
加一个参数
用这个参数为实
例变量赋值
构造器和初始化
Initialization
4 如果你在构造的时候并不很确定 size的值时,应该提供两
个构造器:缺省的和带参的。
public class Duck {
int size;
public Duck() {
size=34;
}
public Duck(int s) {
size=s;
}
}
用户可以根据情况选择
构造的方式;
当你的类里出现带参的
构造器时,编译器不再
为你提供缺省的构造器。
构造器和初始化
Initialization
5 如果类里出现一个以上的构造器,称作 重载 的构造器。
当参数的类型、个数或者顺序不同时,才构成重载。
public class Room {
public Room( int size) { }
public Room() { }
public Room(boolean isOccupied){ }
public Room(boolean isOccupied,int size){ }
public Room(int size,boolean isOccupied){ }
}
overloaded
构造器和初始化
小结
1 构造器是在 new一个对象时运行的代码
Duck d= new Duck() ;
2 构造器必须与类同名,而且无返回类型
public Duck(int size) {.,}
3 如果你不提供,编译器会提供一个缺省的构造器,它总是无参的。
4 可以有多个带不同参数的构造器,每种提供不同的构造方式。多
个构造器的存在意味着出现了重载的构造器。
4.2 继承和多态
? 继承
? 多态
? 子类的初始化
假设在 GUI上有这样
几个图形。设计这样
的程序,
当用户点击某个图形
时,该图形会顺时针
旋转 360度,并播放
一个特定于该图形的
声音文件。
square circle triangle
1 继承的引入
继承
Square
rotate()
playSound()
Circle
rotate()
playSound()
Triangle
rotate()
playSound()
1 这些类存在着共同点
Shape
rotate()
playSound()
2 它们都属于图形,都可以
旋转和播放。把这些共同点
抽离,并组织成一个新的类:
Shape。
Shape
rotate()
playSound()
Square Circle Triangle
superclass
subclass
3 将三个图形类和
新的 Shape类连接
起来,形成如图的
关系叫做继承。
Shape
rotate()
playSound()
Square Circle Triangle
superclass
subclass
Amoeba
rotate()
// amoeba特定
//的 rotate代码
playSound()
//amoeba特定的
//playsound
4 增加一个子类 Amoeba,
它的旋转和播放声音的方
式假设与其他图形是不一
样的,
让 Amoeba类覆盖这两个
方法,覆盖意味着重新定
义,是对源方法进行修改
或扩充的重要手段。
Overriding
methods
2 理解继承概念
1 父类更为抽象,子类更为具体。
父类里包含了子类的公共特征。
雇员
经理 秘书 程序员
董事长
2,子类继承父类”意味着子类继承了父类的所有“成员”,包括实例变量和方法。
Employee
name
salary
Secretary Programmer
superclass
subclass
getSalary()
3
子类可以增加自己的实例变量和方法,更重要
的是,它可以覆盖从父类继承来的方法。
Employee
name
salary
Secretary Manager
superclass
subclass
getSalary()
bonus
getSalary()
新的实例变量
覆盖的方法
4
Java里我们把子类继承父类叫做扩展。
语法,
class A extends B
子类 A
父类 B 扩

例子 public class Doctor {
boolean atHospital;
void treatPatient() { … }
}
public class FamilyDoctor extends Doctor{
boolean makeHouseCalls ;
void giveAdvice () {… }
}
public class Surgeon extends Doctor {
void treatPatient() {…}
void makeIncision() {…}
}
问题,
1 FamilyDoctor和
Surgeon各有几个
实例变量?
2 各有几个方法?
3 FamilyDoctor可
以 treatPatient()吗?
4 FamilyDoctor可
以 makeIncision()吗?
3 继承的好处
1 避免编写重复的代码
把公共的代码放在父类里,让各个子类继承父类的
同时也拥有了该代码。
2 方便、安全的改动代码
如果要改动公共代码的行为,只需要修改父类,子
类的行为自动的改动。而不需要逐个的修改子类代码。
3 为所有的子类定义了一个公共的“合约”。
父类为所有子类提供了必须遵循的特征和行为。
多态
1 类之间的关系
类与类之间有三种基本的关系,
? 依赖 (use-a),类 A的方法中使用了类 B的对象
? 聚合 (has-a),类 A的对象包含了类 B的对象
? 继承 (is-a), 类 A的对象也是类 B的一个对象。
class A{
public A(){
B b=new B();

}
}
class A
{
private B b;
…,
}
class A extends B
{
.,
}
依赖 聚合 继承
子类对象与父类对象的关系, is-a
class A a
class B b
is-a 每个子类对象也是一个父类对
象,即每个 b也是一个 a
例如,
Employee
Manager
员工
经理
每个经理也是一个员工
manager is-a employee
Dog myDog =new Dog() ;
2 多态的具体表现
myDog可以引用其他的 Dog对象,如,
Dog d=new Dog();
myDog=d;
但是 不能引用其他类型的对象,如不可以
象这样,
Dog myDog= new Book();
对象引用 myDog和对象 Dog是 同
类型的 。
有一个例外,即当 引用的类型 是 对象的父类型 时
Animal myDog= new Dog() ;
假设 Animal是 Dog的父类。
在多态的情形下,引用的类型 和 对象的
类型 可以是不同的。
reference
type
object
type
替代规则,只要引用的类型 A是实际对象 B的父类型。可
以用 B替代 A;此时满足 B is-a A;
Animal myDog= new Dog() ;
A B
is-a
Employee e=new Manager(…);
Manager m=new Employee(…)
?
?
替代的单向性,
例子 1 Animal
Dog Cat Lion
Animal[] animal=new Animal[3];
animal[0] = new Dog();
animal[1] = new Cat() ;
animal[2] = new Lion();
你可以把 Animal的任何子
类放进一个 Animal数组。
Animal
的继承

例子 2 class Pet {
public void play( Animal a) {
a.makeNoise();
}
}
class PetOwner{
public void start() {
Pet p=new Pet();
Dog d=new Dog();
Cat c=new Cat();
p.play(d);
p.play(c);
}
}
实参(或者返回值)的类型可以是
“多态”的,即形参 a实际上除了能
接受 Animal类型的实参外,还能接
受所有的 Animal的子类类型。
3 多态的具体表现二 Animal
makeNoise()
eat()
sleep()
roam()
Canine
roam()
Wolf
makeNoise()
eat()
下面调用的是哪个方法?
Wolf w=new Wolf();
w.makeNoise();
w.roam();
w.eat();
w.sleep();
总是调用位于继承
树最低处的方法
更复杂也更关键的情况,
Animal
makeNoise()
eat()
sleep()
roam()
Canine
roam()
Wolf
makeNoise()
eat()
Dog
makeNoise()
eat()
Feline
roam()
Lion
makeNoise()
eat()
Cat
makeNoise()
eat()
Animal[] animal=new Animal[4];
animal[0] = new Dog();
animal[1] = new Cat() ;
animal[2] = new Lion();
animal[3] = new Wolf();
for(int i=0; i<animal.length; i++){
animal[i].eat();
animal[i].roam();
}
当调用
Animal类的
方法时,
JVM会找到
正确的对象,
然后完成调

1
Dog 对象

animal[0].eat();
Animal
1 animal[0]是一个
Animal类型的引用。
2 它实际引用的对
象是子类型 Dog对
象。
3 在编译时,编译
器检查 Animal类的
方法中有没有 eat()
方法。
4 但是在运行时,JVM不是查看引用类型,
而是查看堆中的实际对象。所以如果 Dog类
存在一个 overrided的方法 eat(),JVM负责调
用该版本的 eat();
class Pet {
public void play( Animal a) {
a.makeNoise();
}
}
class PetOwner{
public void start() {
Pet p=new Pet();
Dog d=new Dog();
Cat c=new Cat();
p.play(d);
p.play(c);
}
}

参数虽然只要求是 Animal类型
的,你可以传入各种子类型;
而且,实际的 makeNoise()取决
于堆中的对象。
练习
class A {
void m1() { System.out.print(,A’s m1,”); }
void m2() { System.out.print(“A’s m2,”); }
void m3() { System.out.print(“A’s m3,”); }
}
class B extends A{
void m1() { System.out.print(“B’s m1,”); }
}
class C extends B{
void m3() { System.out.print(“C’s m3,”); }
}
public class Mix{
public static void main(String[] args){
A a=new A();
B b=new B();
C c=new C();
A a2=new C();
下面的输出各是什么?
b.m1();
c.m2();
a.m3();
c.m1();
c.m2();
c.m3();
a.m1();
b.m2();
c.m3();
a2.m1();
a2.m2();
a2.m3();
子类的初始化
public Duck() {
size= 34;
}
public class DuckTest {
public static void main(String[] args) {
Duck d=new Duck() ;
…,
}
}
初始化在构造器里完成
当看到 new时,启动构造器代码
Animal
Dog
a b
i
Animal
makeNoise()
….,
Dog
i
j
a
b
Animal拥有封装的实例变
量。
子类 Dog也有自己的实例
变量,同时还继承了
Animal的所有实例变量。
所以,在创建 Dog对象时,
需要足够容纳两个类的实
例变量的空间。
superclass的构造器,
当创建一个新的对象时,该对象的所有父
类的构造器也必须运行。
Animal Constructor
Canine Constructor
Dog Constructor
Dog d=new Dog();
初始化 Animal部分
初始化 Canine部分
初始化自己
构造器连锁

public class Animal {
public Animal() {
System.out.println(,Making an animal”);
}
}
public class Dog extends Animal {
public Dog() {
System.out.println(,Making an Dog”);
}
}
public class TestDog {
public static void main(String[] args) {
System.out.println(“Starting…”);
Dog d=new Dog();
}
}
> java TestDog
Starting…,
Making an Animal
Making an Dog
执行结果,
解释,
1 当看到 new Dog()时,Dog()构造器进入栈顶 Dog()
2 Dog()激活父类构造器,Animal()被压栈 Animal()
Dog()
3 Animal()执行完之后,被弹出栈顶。 Dog()
怎样 激活 父类构造器?
public class Dog extends Animal{
int size;
public Dog(int newSize){
Animal();
size=newSize;
}
}
是下面这样吗?
wrong!构造器不是方法,不
可以这样调用。
使用 super关键字:唯一调用父类构造器的方式
public class Dog extends Animal{
int size;
public Dog(int newSize){
super();
size=newSize;
}
}
super()使得父类构造器被压
栈,而且是父类的无参的那个
构造器。
如果子类没有提供 super()会怎么样?
1
class Animal {
…,
}
class Dog extends Animal{
public Dog(){
super();
}
}
子类没有构造器
编译器插
入一个构
造器
2 子类的构造器里,没有
super()
class Animal {
…,
}
class Dog extends Animal{
public Dog(){
super();
….,
}
}
编译器插入一
行代码
先有 superclass,再有 subclass
在子类构造器里,super()必须是第一条语句 !
public Boop() {
super();
}
public Boop(int i){
super();
size=i;
}


public Boop() {
}
public Boop(int i){
size=i;
}
public Boop(int i) {
size=i;
super();
}
父类构造器是带参的:意味着父类对象需要一个参数才能完成构造。
public class Animal {
private String name;
public String getName() {
return name;
}
public Animal(String s) {
name=s;
}
}
class Dog extends Animal{
public Dog() {

}
}
public class DogTest{
Dog d=new Dog();
System.out.println(d.getName());
}
编译器只能插入无
参的父类构造器;
父类没有无参的构
造器,编译器无法
插入,构造失败。
Animal需要
一个名字作
为参数
class Dog extends Animal{
public Dog(String s) {
super(s);
}
}
让 super携带参数发送
给父类构造器。
当父类构造器是带参
时,它必须出现在子
类构造器的第一行。
正确的做法,
激活其他重载的构造器的方法:使用 this关键字
class Circle {
int size;
public Circle() {
this(5);
}
public Circle(int s){
size=s;
System.out.println(“Circle size is,+size);
}
}

class Mini extends Car {
Color color;
public Mini() {
this(color.Red);
}
public Mini(Color c) {
super(“mini”);
color=c;
}
public Mini(int size){
this(Color.Red);
super(size);
}
}
缺省构造器。调用重载的
另一个构造器。
完成实际的初始化工作,
调用父类构造器。
this也必须出现在第一行;
因此不允许同时出现 this
和 super!
class Employee{
private double salary;
public double getSalary()
{
return salary;
}

}
class Manager extends Employee{
private bonus;
public double getSalary(){
return salary+bonus;
}
…,
}
补充,super的其他用法
这是有问题的
class Manager extends Employee{
…,
public double getSalary(){
return salary+bonus;
}
…,
}
class Employee{
…,
private double salary;
…,
}
问题在于:如果一个
实例变量是 private的,
即使是它的子类也不
能直接使用它。
class Manager{

public double getSalary(){
double baseSalary=getSalary();
return baseSalary+bonus;
}

}
似乎可以调用公开的
getter方法访问私有实
例变量 …
class Employee{
private double salary;
public double getSalary()
{
return salary;
}

}
class Manager{

public double getSalary(){
double baseSalary=getSalary();
return baseSalary+bonus;
}

}
但是,无法区分是调用
父类的还是子类的
getSalary(),因为这是
一个覆盖的方法。
这样调用,引起了对自
身的循环调用。
class Manager{

public double getSalary(){
double baseSalary=super.getSalary();
return baseSalary+bonus;
}

}
所以,真正的解决
方法是使用 super
关键字明确调用父
类的某个方法。
Animal
Dog
a b
i
super()
尽管父类的成员是作为子
类的一部分而存在,但是
子类要使用这部分不那么
直接。唯一的方式仍然是
通过 super调用。
? 调用父类的方法
? 例如 super.getSalary(…);
? 调用父类构造器
例如,
public Manager (String n,double s,int year,int month,int day)
{
super(n,s,year,month,day);//call Employee’s constructor
bonus=0;
}
super与 this,super
? this是对当前对象的隐式引用。
? super是调用父类方法的关键字,但本身不是对对象的
引用,因而也不能赋值给其他对象;
? 例如,return this;//right
return super;//wrong;
SomeClass self_obj=this;
obj=super;//wrong;
super与 this
练习:哪些子类构造器不能通过编译?
class Boo {
public Boo(int i){ }
public Boo(String s){ }
public Boo(String s,int i){ }
}
class Son extends Boo{
public Son(){
super("boo");
}
public Son(int i){
super("Fred");
}
public Son(String s){
super(42);
}
public Son(int i,String s){
}
public Son(String a,String b,String c){
super(a,b);
}
public Son(int i,int j){
super("man",j);
}
public Son(int i,int x,int y){
super(i,"star");
}
}