第7章 复用类
这一章讲了java中
所谓的复用,就是说代码有可重复使用性。不必每次遇到问题都重新写代码,这也是OOP语言的一大优点。而这里的复用方法,总的来说有两种,继承 和 组合。
7.1 组合的语法
package day_44;
// 这个是 组合 的例子
class WaterSource{
private String s;
WaterSource(){
System.out.println("WaterSource()构造器");
s = "已经完成构造";
}
public String toString(){ return s;}
}
public class SprinklerSystem {
private String value1,value2,value3,value4;
private WaterSource source = new WaterSource();
private int i;
private float f;
public String toString(){
return
"value1 = "+value1+" "+
"value2 = "+value2+" "+
"value3 = "+value3+" "+
"value4 = "+value4+"\n"+
"source ="+ source;
}
public static void main(String args[]){
SprinklerSystem s = new SprinklerSystem();
System.out.println(s);
}
}
/* output
WaterSource()构造器
value1 = null value2 = null value3 = null value4 = null
source =已经完成构造
*/
上面的代码中,需要注意:在 SprinklerSystem 类中的 toString() 方法中,最后return "source="+source这里
的source对象需要是一个 String对象,而在WaterSource类中, 编译器会自动调用 toString() 方法,把 source对象转换成一个 String 对象。
其实在所有的非基本类中,都有一个 toString() 方法。所以你在编写新的类时候,也可以这么写。
7.2 继承的语法
- 继承的例子
package day_44;
class Cleanser{ // Cleanser 清洁剂
private String s = "清洁剂";
public void append(String a){s += a;}
public void dilute(){ append(" 稀释");}
public void apply(){append(" apply()");}
public void scrub(){append(" 擦洗");}
public String toString(){return s;}
public static void main(String args[]){
Cleanser x = new Cleanser();
x.dilute();
x.apply();
x.scrub();
System.out.println(x);
}
}
public class Detergent extends Cleanser{ // detergent 洗衣粉
public void scrub(){ // 改写父类的方法
append(" 洗衣粉的 scrub()方法");
super.scrub(); // 调用父类的方法
}
public void foam(){append(" foam()");} // 添加新的方法
public static void main(String args[]){
Detergent d = new Detergent();
d.dilute();
d.apply();
d.scrub();
d.foam();
System.out.println(d);
System.out.println("Testing base class:");
Cleanser.main(args); // 把 命令行获得的参数给 基类的 main() 方法
}
}
/* output
清洁剂 稀释 apply() 洗衣粉的 scrub()方法 擦洗 foam()
Testing base class:
清洁剂 稀释 apply() 擦洗
*/
这里需要注意两个点:
- super关键字:调用父类的方法
- 两个类都有 main() 方法,这样写有利于每个类进行单元测试。在Detergent 类中调用 main()函数,这里同时会调用 基类的 main()函数。
- 继承类会自动调用基类的构造函数,不需要显示的写出来。
package day_44;
class Art{
Art(){System.out.println("Art的构造器");}
}
class Video extends Art{
Video(){System.out.println("Video的构造器");}
}
public class Cartoon extends Video{
public Cartoon(){System.out.println("Cartoon的构造器");}
public static void main(String args[]){
Cartoon c = new Cartoon();
}
}
/*output
Art的构造器
Video的构造器
Cartoon的构造器
*/
- 含参的构造函数需要显示的用 super 关键字写出来。
package day_44;
class Game{
Game(int i){
System.out.println("Game的构造器");
}
}
class BoardGame extends Game{
BoardGame(int i){
super(i);
System.out.println("BoradGame的构造器");
}
}
public class Chess extends BoardGame{
Chess(int i){
super(i);
System.out.println("Chess 的构造器");
}
public static void main(String args[]){
Chess c = new Chess(11);
}
}
/*output
Game的构造器
BoradGame的构造器
Chess 的构造器
*/
7.4 代理
所谓的“代理”是 继承和组合的中庸之道。在 java 中并没有明确提出对代理的支持,但是在一些 IDE 中可以自动生成代理。这里直接看代码
package day_45;
class SpaceShipControls{
public void up(int velocity){}
public void down(int velocity){}
public void foward(int velocity){}
public void left(int velocity){}
public void right(int velocity){}
public void back(int velocity){}
public void turboboost(){}
}
public class SpaceShopDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShopDelegation(String name){ // 构造函数
this.name = name;}
// 代理的方法
public void back(int velocity){
controls.back(velocity);
}
public void right(int velocity){
controls.right(velocity);
}
//...剩下的方法都可以这么写
public static void main(String args[]){
SpaceShopDelegation s = new SpaceShopDelegation("MikeSpaceShip");
s.back(100);
}
}
可以看到,这里所谓的代理,在实现上,是在新类中初始化一个“已有类”对象,通过调用这个对象的部分方法(子集),来达到更高的控制力。
个人理解:因为组合是用全部的方法,继承的写法又要 extends 关键字。所以这里的代理就是不用 extends关键字的继承。
这一个小节大概看一下,不要对已有的继承和组合产生 weaken。知道有一个东西叫做代理就行了。
7.4 结合使用组合和继承
实际问题中,结合使用二者是很常见的事情,两种技术通常都被使用来生成更加复杂的类。还是直接看代码。
package day_45;
class Plate{
Plate(int i){System.out.println("Plate构造器");}
}
class DinnerPlate extends Plate{
DinnerPlate(int i){
super(i);
System.out.println("DinnerPlate构造器");
}
}
class Utensil{// 家里的用具、器皿
Utensil(int i){System.out.println("Utensil构造器");}
}
class Spoon extends Utensil{
Spoon(int i){
super(i);
System.out.println("Spoon构造器");
}
}
class Knife extends Utensil{
Knife(int i){
super(i);
System.out.println("Knife构造器");
}
}
class Fork extends Utensil{
Fork(int i){
super(i);
System.out.println("Fork构造器");
}
}
// 做某事的方式
class Custom{
Custom(int i){
System.out.println("Custom构造器");
}
}
public class PlaceSetting extends Custom{
private Spoon sp;
private Fork frk;
private Knife kn;
private DinnerPlate pl;
public PlaceSetting(int i){
super(i+1);
sp = new Spoon(i+2);
frk = new Fork(i+3);
kn = new Knife(i+4);
pl = new DinnerPlate(i+5);
System.out.println("PlaceSetting构造器");
}
public static void main(String args[]){
PlaceSetting p = new PlaceSetting(9);
}
}
/* output
Custom构造器
Utensil构造器
Spoon构造器
Utensil构造器
Fork构造器
Utensil构造器
Knife构造器
Plate构造器
DinnerPlate构造器
PlaceSetting构造器
*/
上面的代码中,需要注意构造器的执行顺序,和之前一样,先执行基类的构造器,再执行继承类的构造器。
7.4.1 确保正确清理
这里是说在某些情况下,虽然java中有辣鸡清理器,但是还是需要我们显示的写一个清理函数。之前也说过finalize() 方法。这里是析构函数的意思,跟finalize还不一样。如果真的有需要手动清理的情况,一定要自己写方法,不要用 finalize。
package reusing;
class Shape{
Shape(int i){
System.out.println("Shape构造器");
}
void dispose(){System.out.println("Shape析构");}
}
class Circle extends Shape{
Circle(int i){
super(i);
System.out.println("Drawing Circle");
}
void dispose(){System.out.println("Erasing Circle");
super.dispose();
}
}
class Line extends Shape{
private int start,end;
Line(int start,int end){
super(start);
this.start = start;
this.end = end;
System.out.println("Drawing Line:"+start+", "+end);
}
void dispose(){
System.out.println("Erasing Line");
super.dispose();
}
}
public class CADSystem extends Shape{
private Circle c;
private Line[] lines = new Line[3]; // Line 数组
public CADSystem(int i){
super(i+1);
for(int j=0;j<lines.length;j++){
lines[j] = new Line(j,j*j); // 对数组中每一个 Line 元素初始化
}
c = new Circle(3);
System.out.println("组合构造器");
}
void dispose(){
System.out.println("CADSys析构器");
// 注意这里的析构器顺序跟构造器是相反的
c.dispose();
for(int i=lines.length-1;i>=0;i--){
lines[i].dispose(); // 从后向前
}
super.dispose();
}
public static void main(String args[]){
CADSystem x = new CADSystem(47);
try {
// 一些操作
}finally {
x.dispose();
}
}
}
/*output
Shape构造器
Shape构造器
Drawing Line:0, 0
Shape构造器
Drawing Line:1, 1
Shape构造器
Drawing Line:2, 4
Shape构造器
Drawing Circle
组合构造器
CADSys析构器
Erasing Circle
Shape析构
Erasing Line
Shape析构
Erasing Line
Shape析构
Erasing Line
Shape析构
Shape析构
*/
输出顺序很简单,照着代码顺一遍就成了。
这里的 try{}finally{},是保护区代码块。意思是,不管 怎样,finally{}里面的代码一定要执行,就是说这里的清理时一定要执行的。
7.4.2 名称屏蔽
java 中的积累有用某个已经被多次重载的方法名称,在到处累中重新定义该方法名称并不会屏蔽其在基类中的任何版本。(c++中会产生名称屏蔽)
package reusing;
class Homer{
char doh(char c){
System.out.println("doh(char)");
return 'd';
}
float doh(float f){
System.out.println("doh(float)");
return 1.0f;
}
}
class Milhouse{}
class Bart extends Homer{
void doh(Milhouse m){
System.out.println("doh(Milhouse)");
}
}
public class Hide {
public static void main(String args[]){
Bart b = new Bart();
b.doh(1);
b.doh('x');
b.doh(1.0f);
b.doh(new Milhouse());
}
}
/* output
doh(float)
doh(char)
doh(float)
doh(Milhouse)
*/
记住,java中重载并不会产生名称屏蔽,都可以调用。
这里需要区分两个概念:重写override 和 重载 overload。上面代码中的是重载,就是说同名,不同参。而重写是说,参数相同,只是具体实现的代码不同,重新写一个。
java SE5中增加了 @override 注解,写在方法前面,有两个好处:
- 当注解用。
- 让编译器检查父类是否有同名方法,没有则报错。
7.5 在组合和继承之间选择
还是我们之前说的has-a 和 is-a 之间的关系。
package reusing;
class Engine{
public void start(){}
public void rev(){}
public void stop(){}
}
class Wheel{
public void inflate(int psi){}
}
class Window{
public void rollup(){}
public void rolldown(){}
}
class Door{
public Window window = new Window();
public void open(){}
public void close(){}
}
public class Car {
public Engine engine = new Engine();
public Wheel[] wheels = new Wheel[4]; // 4 wheels
public Door
left = new Door(),
right = new Door(); // 2 doors
public Car(){
for(int i=0;i<wheels.length;i++){
wheels[i] = new Wheel();
}
}
public static void main(String args[]){
Car car = new Car();
car.left.window.rollup();
car.wheels[0].inflate(72);
}
}
7.6 protected 关键字
之前说过,protected 关键字是在继承的情境下发挥作用的。protected说明,就类用户而言,是private的,但是就其导出类而言,是可以访问的。
package reusing;
class Villain{ // 坏蛋,反派
private String name;
protected void set(String nm){name=nm;}
public Villain(String name){this.name = name;}
public String toString(){
return "I'm a Villain and my name is "+ name;
}
}
public class Orc extends Villain{
private int orcNumber;
public Orc(String name,int orcNumber){
super(name); // 显式调用父类的构造函数
this.orcNumber = orcNumber;
}
public void change(String name, int orcNumber){
set(name); // 由于是 proteced ,所以继承类可以调用
this.orcNumber = orcNumber;
}
public String toString(){
return "Orc "+orcNumber +": "+super.toString();
}
public static void main(String args[]){
Orc orc = new Orc("Mike",001);
System.out.println(orc);
orc.change("Shine",002);
System.out.println(orc);
}
}
7.7 向上转型 upcasting
之前说过的向上转型。
package reusing;
class Instrument{
public void play(){}
static void tune(Instrument i){ // 静态方法可以不用对象,直接调用
// 调音的方法
i.play();
}
}
public class Wind extends Instrument {
public static void main(String[] args){
Wind flute = new Wind();
Instrument.tune(flute); // 这里自动向上转型
}
}
7.8 final 关键字
根据上下文的含义,java中final关键字的含义存在着细微的区别,但通常它指的是“无法改变的”。可能是出于设计/效率的理由。可能用到final的情况:数据、方法、类(即都可以)。
7.8.1 final数据
- final 基本类型
final 基本类型,告诉编译器此变量是恒定不变的,这时候编译器可以在编译时执行计算式,从而提高效率。
final常量通常和static一起用,强调只有一份,这时候变量名大写下划线隔开:MIKE_SHINE。
其实这里对于 final 基本类型,看了书上的东西,越发的不明白了。这里参考该博主的文章,对final变量和普通变量做区分。
1.类的 final变量 和 普通变量有什么区别。
当final变量是基本数据累心个String类型时,如果在编译期间可以知道它的确切值,则编译器会把它当做编译期常量使用。所以代码在使用到 b 变量时候,相当于直接替换成为 “hello”。这是一种优化。public class Test45 { public static void main(String args[]){ String a = "hello2"; final String b = "hello"; String d = "hello"; String c = b + 2; String e = d + 2; System.out.println((a == c)); // 实际上 c和e指向的值 都是"hello2" System.out.println((a == e)); // 但是这里是在比较两个引用,指向的地址不同,所以是 false System.out.println(a.equals(e)); } } /*output true false true*/
同理,而如果编译时候不能获取确切值,就不会做上面的优化。看下面的代码
public static void main(String[] args) { String a = "hello2"; final String b = getHello(); String c = b + 2; System.out.println((a == c)); } public static String getHello() { return "hello"; } } /*output false */
- final 和 static 的区别
之前在第5章中提到过,初始化的顺序是先是 static 变量,再是变量,最后才是构造函数。而 static 变量只初始化一份。
这里同样,static作用于成员变量只表示存一份副本,final作用是保证变量不可变。public class Test { public static void main(String[] args) { MyClass myClass1 = new MyClass(); MyClass myClass2 = new MyClass(); System.out.println(myClass1.i); System.out.println(myClass1.j); System.out.println(myClass2.i); System.out.println(myClass2.j); } } class MyClass { public final double i = Math.random(); public static double j = Math.random(); }
输出结果发现 两个对象的 j 是相同的,而i是不同的。
- final 对象引用
引用恒定不变,一旦引用初始化指向一个对象,无法再把它改变为指向另外一个对象。但是对象本身是可以改变的(可以用c++里面的指针来理解)。之前提过,java数组可以看做是引用。
package reusing;
import java.util.Random;
class Value{
int i;
public Value(int i){this.i = i;}
}
public class FinalData {
private static Random rand = new Random(47); //一个随机数
private String id;
public 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;
// 下面两个都 不是 编译期常量,运行时才产生
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22); // final 对象
private static final Value VAL_3 = new Value(33);
// 数组
private final int[] a = {1,2,3,4,5,6};
public String toString(){
return id+": "+"i4 = "+i4+", INT_5 = " + INT_5;
}
public static void main(String args[]){
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; // 错,值不能变
fd1.v2.i++;
fd1.v1 = new Value(9); // v1可以指向其他对象,不是final对象
for(int i=0;i<fd1.a.length;i++){
fd1.a[i]++; // 数组中的元素可变,元素不是final的
}
//! fd1.ve = new Value(9) // 错,final对象引用
//! fd1.a = new int[3] // 不能指向新的对象
System.out.println(fd1);
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
}
/*output
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*/
上面的代码中,其他的都比较简单,注意一下 final 和 static final 的区别:final 在每个对象中唯一,static final 在多个对象中唯一(只有一份)。可以看到,i4在两个对象中不同,而INT_5在两个对象中相同。
- final 参数
指明为 final 的方法参数,意味着方法内只能读而不能修改参数,这个特性主要用来向匿名内部类传递数据(第10章会学习)。
package reusing;
class Gizmo{ // 小玩意
public void spin(){}
}
public class FinalArgu {
void with(final Gizmo g){
//! g = new Gizmo(); // 参数不能改
}
void without(Gizmo g){
g = new Gizmo();
g.spin();
}
int g(final int i){return i+1;}
public static void main(String args[]){
FinalArgu fa = new FinalArgu();
fa.without(null);
fa.with(null);
}
}
- final 方法
使用final方法的原因有两个:
-- 锁定方法。防止任何继承类修改方法。出于设计的考虑
--效率。
final 和 private 关键字
-- 类中所有的 private 方法其实都隐式地指定为是 final 的,因为在继承类中无法访问到这些方法,自然也就无法覆盖、修改这些方法
-- 派生类中试图“覆盖”父类中的 private 方法,编译没错。但是实际上你只是给派生类了一个新的方法。
- final 类
不希望有子类,不希望被继承的类
7.9 初始化及类的加载
7.9.1 类的加载
java 中采用了一种不同的对类的加载方式,java的每个类的编译代码都存在于自己的独立文件(.class文件)中。该文件旨在其代码被使用时候才被加载。通常,可以说“类的代码在初次使用时才加载”。更准确的说,类是在其任何 statci 成员被访问时加载的
7.9.2 类的初始化
初次使用时,static就初始化。所有的 static 对象和代码段依照顺序初始化。定义为 static 的只初始化一次。(也就是我们之前说的 static final 在多个对象中唯一。)
package reusing;
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 initialized");
static int printInit(String s){
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect{
private int k = printInit("Beetle.k initialized");
public Beetle(){
System.out.println("k = "+k);
System.out.println("j = "+j);
}
private static int x2 = printInit("static Insect.x2 initialized");
public static void main(String args[]){
System.out.println("Beetle 构造器");
Beetle b = new Beetle();
}
}
/*output
static Insect.x1 initialized
static Insect.x2 initialized
Beetle 构造器
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*/
这里要注意的就是加载的顺序。
首先加载的是 Beetle.main()方法(static方法),加载器启动寻找Beetle类的编译代码,并且发现,其有基类 Insect,于是加载Insect类。接下来,根基类Insect 中的static初始化,然后是导出类 Beetle 中的 static。之后才是 Beetle.main(),所以最终输出顺序如上。
网友评论