七、 复用类
使用类而不破坏现有程序代码,有两种方法。
- 只需在新的类中产生现有类的对象。由于新类是由现有类的对象组成,所以这种方法称为组合。该代码只是复用了现有程序代码的功能,而非它的形式。
- 按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式在其中添加新代码。即为继承,而且编译器可已完成其中大部分工作。
7.1 组合语法
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立
优点:具有较好的可扩展性
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口
缺点:整体类不能自动获得和局部类同样的接口
缺点:创建整体类的对象时,需要创建所有局部类的对象
public class WaterSourse {
private String s;
WaterSourse(){
System.out.println("WaterSourse()");
s = "Constructed";
}
public String toString (){
return s;
}
}
public class SprinklerSystem {
private String value1,value2,value3,value4;
private WaterSourse source = new WaterSourse();
private int i;
private float f;
public String toString(){
return "value1 = "+ value1 +" "+
"value2 = "+ value2 +" "+
"value3 = "+ value3 +" "+
"value4 = "+ value4 +"\n"+
"i = "+ i +" "+
"f = "+ f +" "+
"source = "+source;
}
public static void main(String[] args) {
SprinklerSystem s = new SprinklerSystem();
System.out.println(s);
}
}
//output
WaterSourse()
value1 = null value2 = null value3 = null value4 = null
i = 0 f = 0.0 source = Constructed
toString():每一个非基本类型的对象都有一个toString()方法,而且当编译器需要一个String而你却只有一个对象时,该方法便会被调用。(这里可以看一下源码,调用打印方法的时候又调用了)
public void println(Object x){
String s = String.valueOf(x);
synchronized (this) {
print(s); newLine();
}
public void print(Object obj) {
write(String.valueOf(obj));
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
编译器并不是简单的为每一个引用都创建默认对象。如果想初始化这些引用,可在代码的下列位置进行
1.定义对象的地方,这意味着他们总是能够在构造器被调用之前被初始化。
2.在类的构造器中
3.就在正要使用这些对象之前 (惰性初始化)。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担
public String toString(){
if(s==null)
s = "srq";
}
4.使用实例初始化
练习1:创建一个简单的类。在第二个类中,将一个引用定义为第一个类的对象,运用惰性初始化来实例化这个对象。
public class Test1 {
}
public class Test1plus {
private Test1 t;
@Override
public String toString() {
if(t == null){
System.out.println("实例化");
t = new Test1();
}
return "t = "+ t;
}
public static void main(String[] args) {
Test1plus tp = new Test1plus();
System.out.println(tp);
}
}
7.2 继承语法
优点:创建子类的对象时,无须创建父类的对象
优点:子类能自动继承父类的接口
缺点:子类不能改变父类的接口
缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性
缺点:不支持动态继承。在运行时,子类无法选择不同的父类
缺点:支持扩展,但是往往以增加系统结构的复杂度为代价
在类名后添加关键字extends而实现,这么做时会自动得到基类中所有的域和方法。
public class Father {
private String s = "Father";
public void append(String a ){
s += a;
}
public void apply(){
append("apply()");
}
public void abc(){
append("abc()");
}
public String toString() {
return s;
}
public static void main(String[] args) {
Father f = new Father();
f.apply();
f.abc();
System.out.println(f);
}
}
public class Son extends Father{
//重写父类方法
public void apply(){
append("Son.apply()");
super.apply();//super关键字表示超类的意思,调用父类的方法
}
//新增方法
public void foam(){
append("foam()");
}
public static void main(String[] args) {
Son s = new Son();
s.apply();
s.foam();
s.abc();//可调用父类的方法
System.out.println(s);
System.out.println("Test Base class");
Father.main(args);
}
}
//output
FatherSon.apply()apply()foam()abc()
Test Base class
Fatherapply()abc()
练习二:从Son中继承产生一个新的类。覆盖foam()放发并添加一个名为sterilize()的新方法。
public class Grandson extends Son{
public void foam(){
append("Grandson.foam()");
}
public void sterilize(){
append("sterilize()");
}
public static void main(String[] args) {
Grandson g = new Grandson();
g.foam();
g.sterilize();
System.out.println(g);
}
}
7.2.1 初始化基类
构建过程是从基类“向外扩散”,所以基类在导出类构造器可以访问它之前,就已经完成了初始化。即使你不为Cartoon()创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。
public class Art {
Art(){
System.out.println("Art初始化");
}
}
public class Drawing extends Art{
Drawing(){
System.out.println("Drawing初始化");
}
}
public class Cartoon extends Drawing{
Cartoon(){
System.out.println("Cartoon初始化");
}
public static void main(String[] args) {
Cartoon c = new Cartoon();
}
}
练习3:证明上述的观点
只需把上述构造方法注释掉运行即可,发现基类都已完成初始化。
练习4:证明基类构造器:总是会被调用;在导出类构造器之前被调用
与上例相同
练习5:创建两个带有默认构造器(空参数列表)的类A和类B。从A中继承产生一个名为C的新类;并在C内创建一个B类的成员。不要给C编写构造器。创建一个C类的对象并观察其结果。
public class A {
public A(){
System.out.println("A初始化");
}
}
public class B {
B(){
System.out.println("B初始化");
}
}
public class C extends A {
private B b = new B();
public static void main(String[] args) {
C c = new C();
}
}
带参数的构造器
想要调用一个带参数的基类构造器,就必须用super显示地编写调用基类构造器的语句,并且配以适当的参数列表,必须在第一行。
public class Game {
Game(int i){
System.out.println("Game 初始化"+i);
}
}
public class BorderGame extends Game{
BorderGame(int i){
super(i);
System.out.println("BorderGame初始化"+i);
}
}
public class Chess extends BorderGame {
Chess(){
super(11);
System.out.println("Chess初始化");
}
public static void main(String[] args) {
Chess c = new Chess();
}
}
如果不调用基类构造器,编译器将会报错无法找到符合Game()形式的构造器。而且调用基类构造器必须是你在导出类构造器中要做的第一件事(若错了编译器会提醒你)。
练习6:用Chess.java来证明前一段话
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7aGDPfI-1599637653662)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1592374852822.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I1L6boNf-1599637653664)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1592374887130.png)]
练习7:修改练习5,使A和B以带参数的构造器取代默认构造器。为C写一个构造器,并在其中执行所有的初始化。
public class A {
public A(int i){
System.out.println("A初始化"+i);
}
}
public class B {
B(int i){
System.out.println("B初始化"+ i);
}
}
public class C extends A {
C(){
super(11);
System.out.println("C初始化");
}
private B b = new B(6);
public static void main(String[] args) {
C c = new C();
}
}
练习8:创建一个基类,它仅有一个非默认的构造器;再创建一个导出类,它带有默认构造器和非默认构造器。在导出类的构造器中调用基类的构造器。
public class Test8 {
Test8(int i){
System.out.println("Test8初始化"+i);
}
}
public class Test8Son extends Test8 {
Test8Son(){
super(1);
}
Test8Son(int i){
super(2);
System.out.println("Test8Son有参"+i);
}
public static void main(String[] args) {
Test8Son t = new Test8Son();
Test8Son t2 = new Test8Son(3);
}
}
练习9:创建一个Root类,令其含有名为Component1、Component2、Component3的类的一个实例。从Root中派生一个类Stem,也含有上述各“组成部分”。所有的类都应该带有可打印出类的相关信息的默认构造器。
public class Root {
public Component1 c1 = new Component1();
public Component2 c2 = new Component2();
public Component3 c3 = new Component3();
Root(){
System.out.println("Root初始化");
}
}
public class Stem extends Root {
Stem(){
System.out.println("Stem初始化");
}
public Component1 c1 = new Component1();
public Component2 c2 = new Component2();
public Component3 c3 = new Component3();
public static void main(String[] args) {
Stem s = new Stem();
}
}
//output
Component1初始化
Component2初始化
Component3初始化
Root初始化
Component1初始化
Component2初始化
Component3初始化
Stem初始化
7.3 代理
继承于组合之间的中庸之道,在代理类定义了成员变量,定义了基类的所有方法(通过基类调用的父类方法);类似于中介拿着房东的资源给住户。解决了父类方法暴露在子类中。
public class SonDelegation {
private Father f = new Father();
public void append(String s){
f.append(s);
}
@Override
public String toString() {
return f.toString();
}
public void foam(){
append("foam()");
}
public static void main(String[] args) {
SonDelegation s = new SonDelegation();
s.apply();
s.foam();
s.abc();
s.append("srq");
System.out.println(s);
}
private void abc() {
append("abc()");
}
private void apply() {
append("applly()");
}
}
组合、继承和代理三者的定义:
- 组合:在新类中new 另外一个类的对象,以添加该对象的特性。
- 继承:从基类继承得到子类,获得基类的特性。
- 代理:在代理类中创建某功能的类,调用类的一些方法以获得该类的部分特性。
使用场合:
- 组合:各部件之间没什么关系,只需要组合即可。like组装电脑,需要new CPU(),new RAM(),new Disk()……
- 继承:子类需要具有父类的功能,各子类之间有所差异。like Shape类作为基类,子类有Rectangle,CirCle,Triangle……代码不写了,大家都经常用。
- 代理:飞机控制类,我不想暴露太多飞机控制的功能,只需部分前进左右转的控制(而不需要暴露发射导弹功能)。通过在代理类中new一个飞机控制对象,然后在方法中添加飞机控制类的各个需要暴露的功能。
7.4 结合使用组合和继承
在一个类上同时定义了成员对象同时也继承了基类。
public class Plate {
Plate(int i){
System.out.println("Plate 初始化"+i);
}
}
public class DinnerPlate extends Plate {
DinnerPlate(int i){
super(i);
System.out.println("DinnerPlate初始化"+i);
}
}
public class Utensil {
Utensil(int i){
System.out.println("Utensil初始化"+i);
}
}
public class Spoon extends Utensil {
Spoon(int i){
super(i);
System.out.println("Spoon初始化"+i);
}
}
public class Fork extends Utensil {
Fork(int i){
super(i);
System.out.println("Fork初始化"+i);
}
}
public class Custom {
Custom(int i){
System.out.println("Custonm初始化"+i);
}
}
public class PlaceSetting extends Custom {
private Spoon spoon;
private Fork fork;
private DinnerPlate dinnerPlate;
PlaceSetting(int i){
super(i+1);
spoon = new Spoon(i+2);
fork = new Fork(i+3);
dinnerPlate = new DinnerPlate(i+4);
System.out.println("PlaceSetting初始化"+i);
}
public static void main(String[] args) {
PlaceSetting ps = new PlaceSetting(1);
}
}
//output
Custonm初始化2
Utensil初始化3
Spoon初始化3
Utensil初始化4
Fork初始化4
Plate 初始化5
DinnerPlate初始化5
PlaceSetting初始化1
7.4.1 确保正确清理
类可能要在其生命周期内执行一些必须的清理活动,因为垃圾回收器不知何时将会被调用,或者它是否将被调用,所以用到了finally字句,以防异常的出现。(其中finally字句表示无论发生什么事,都一定会执行)
练习12: 将一个适当的dispose()方法的层次结构添加到练习9的所有类中
public class Component1 {
Component1(){
System.out.println("Component1初始化");
}
void dispose(){
System.out.println("Component1清理");
}
}
public class Component2 {
Component2(){
System.out.println("Component2初始化");
}
void dispose(){
System.out.println("Component2清理");
}
}
public class Component3 {
Component3(){
System.out.println("Component3初始化");
}
void dispose(){
System.out.println("Component3清理");
}
}
public class Root {
public Component1 c1 = new Component1();
public Component2 c2 = new Component2();
public Component3 c3 = new Component3();
Root(){
System.out.println("Root初始化");
}
void dispose(){
System.out.println("Root清理");
}
}
public class Stem extends Root {
Stem(){
System.out.println("Stem初始化");
}
void dispose(){
super.dispose();
c1.dispose();
c2.dispose();
c3.dispose();
System.out.println("Stem清理");
}
public Component1 c1 = new Component1();
public Component2 c2 = new Component2();
public Component3 c3 = new Component3();
public static void main(String[] args) {
Stem s = new Stem();
try {
}finally {
s.dispose();
}
}
}
//output
Component1初始化
Component2初始化
Component3初始化
Root初始化
Component1初始化
Component2初始化
Component3初始化
Stem初始化
Root清理
Component1清理
Component2清理
Component3清理
Stem清理
7.4.3 名称屏蔽
如果java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称的并不会屏蔽其在基类中的任何版本,无论在该层或者它基类中对方法进行定义,重载机制都可以正常工作。
@Override这个注解可以用来检测你是重写此方法而不是重载
public class Homer {
char doh(char c){
System.out.println("doh(char)");
return c;
}
float doh(float f){
System.out.println("doh(float)");
return f;
}
}
public class Minhouse {
}
public class Bart extends Homer {
void doh(Minhouse minhouse){
System.out.println("doh(Minhouse)");
}
}
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 Minhouse());
}
}
//output
doh(float)
doh(char)
doh(float)
doh(Minhouse)
练习13:创建一个类,它应带有一个被重载了三次的方法。继承产生一个新类,并添加一个该方法的重载定义,展示四个方法在导出类中都是可以使用的。
同上例相同
7.5 在组合与继承之间选择
可以自己去想象一下现实生活中的例子比如说你要组装一个车轮子,车门啥的这个时候用组合就很明显了,当你针对车子比如说奥迪,奔驰等等这些车子都有着共同的特性与属性例如能跑都有轮子门啥的。
组合是一种has a 的关系
继承则是一种is a 的关系
练习14:在Car.java中给Engine添加一个service()方法,并在main()中调用该方法
public class Engine {
public void start(){
}
public void rev(){}
public void stop(){}
public void service(){
System.out.println("service....");
}
}
public class Whell {
public void inflate(int psi){
}
}
public class Window {
public void rollup(){}
public void rolldown(){}
}
public class Door {
public Window window = new Window();
public void open(){}
public void close(){}
}
public class Car {
public Engine engine = new Engine();
public Whell[] whells = new Whell[4];
public Door left = new Door();
public Door right = new Door();
public Car(){
for(int i= 0;i<whells.length;i++){
whells[i] = new Whell();
}
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rollup();
car.whells[0].inflate(72);
car.engine.service();
}
}
7.6 protected关键字
protected关键字:对于任何继承于此类的导出类或其它任何位于同一个包内的类来说,都是可以访问的。
练习15:在包中编写一个类,类应具备一个protected方法。在包外部,试着调用该protected方法并解释其结果。然后从你的类中继承产生一个类,并从该导出类的方法内部调用该protected方法。
public class Test15 {
protected void service(){
System.out.println("protected方法");
}
}
public class Test15Son extends Test15 {
public static void main(String[] args) {
Test15Son t = new Test15Son();
t.service();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yd4sC6Oh-1599637653665)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1592388380126.png)]
7.7 向上转型
父类引用指向子类对象。
public class Instrument {
public void play(){
}
static void tune(Instrument i){
i.play();
}
}
public class Wind extends Instrument {
public static void main(String[] args) {
Wind wind = new Wind();
Instrument.tune(wind);
}
}
此例中,tune()方法可接受Instrument引用。但在Wind.main()中,传递给tune()方法的是一个Wind引用。其实Wind对象同样是一种Instrument对象,而且也不存在任何tune()方法是可以通过Instrument来调用,同时又不存在于Wind中。在tune()中,程序代码可以对Instrument引用的动作,我们称之为向上转型。
7.7.1 为什么称向上转型
导出类转型成基类
7.7.2 再论组合与继承
如果新类必须向基类向上转型,则继承是必要的。
练习16:创建一个名为Amphibian的类。由此继承产生一个称为Frog的类,在基类中设置适当的方法。在main()中,创建一个Frog并向上转型至Amphibian,然后说明所有方法都可工作。
public class Amphibian {
static void service(Amphibian amphibian){
System.out.println("ji"+amphibian);
}
}
public class Frog extends Amphibian {
public static void main(String[] args) {
Frog f = new Frog();
Amphibian.service(f);
}
}
练习17:修改练习16,使Frog覆盖基类中方法的定义(令新定义使用相同的方法特征签名)。请留心main()中都发生了什么。
public class Amphibian {
void mating(){
System.out.println("两栖动物交配");
}
static void service(Amphibian amphibian){
System.out.println("ji"+amphibian);
}
}
public class Frog extends Amphibian {
void mating(){
System.out.println("青蛙交配");
}
public static void main(String[] args) {
Frog f = new Frog();
Amphibian a = new Frog();
Amphibian.service(f);
a.mating();
f.mating();
}
}
//output(如果子类不重写输出的就是两栖动物交配)
jicom.agree.SevenZ.Frog@1540e19d
青蛙交配
青蛙交配
7.8 final关键字
通常含义指的是“这是无法改变的”针对final的场景:数据,方法和类
7.8.1 final数据
1.一个永远不变的编译时常量
2.一个在运行时被初始化的值,而你不希望它被改变
这类常量必须是基本数据类型,并且以关键字final表示。在对这个常量进行 定义的时候,必须对其进行赋值。
一个既是static又是final的域只占据一段不能改变的存储空间。
当对象引用运用final,一旦引用被初始化指向一个对象,就无法再把它改为指向另一个对象,对象其自身确实可以被修改的,这一限制同样适用数组,它也是对象。
练习18:创建一个含有static final域和final域的类,说明二者间的区别。
public class Value {
Value(int i){
System.out.println("Value值 "+i);
}
}
public class FinalData {
public final int value1 = 1;
public static final int VALUE2 = 2;
private final Value v1 = new Value(3);
private static final Value V2 = new Value(4);
private final int[] a = {1,2,3,4,5};
public String toString(){
return "value1= "+value1+" "+"VALUE2= "+VALUE2+" "+"v1= "+v1+"V2= "+V2;
}
public static void main(String[] args) {
FinalData f = new FinalData();
//f.v1 = new Value(6);//报错
for(int i =0;i<f.a.length;i++){
//f.a[i] = 2;//编译通过
System.out.print(f.a[i]);
}
System.out.println(f);
}
}
//static随着类的加载就已经初始化了
空白final
声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前必须被初始化
练习19:创建一个含有指向某对象的空白final使用类。在所有构造器内部都执行空白final初始化动作。说明java确保final在使用前必须被初始化,且一旦被初始化既无法改变
private final Value v3;
private final int j;
FinalData(){
v3 = new Value(8);
j = 66;
}
//我直接接着上一个类改的
final参数
java允许在参数列表中以声明的方式将参数指明为final,这意味着你无法在方法中更改参数引用所指向的对象。
这一特性主要用来向匿名内部类传递数据。
7.8.2 final方法
用法
1.将方法锁定,以防任何继承类修改它的含义,确保在继承中使方法行为保持不变,并且不会被覆盖
2.效率,方法指明为final就是同意编译器将针对该方法的所有调用都转为内嵌调用。但是在最新的java版本虚拟机(hotspot)技术优化了。
final和private关键字
生成导出类,你去重写基类方法你会发现根本没有存在覆盖,仅是生成了一个新的方法。
练习20:展示@Override注解可以查询他是否为重写。
练习21:创建一个带final方法的类。由此继承产生一个类并尝试覆盖该方法。
public class FinalData {
private final void fiservice(){
System.out.println("fiservice....");
}
}
public class FinalDataSon extends FinalData {
//注解这是报红的
@Override
private final void fiservice(){
System.out.println("fiserviceson....");
}
public static void main(String[] args) {
FinalDataSon f = new FinalDataSon();
f.fiservice();
}
}
7.8.3 final类
fianl定义在类上标明此类无法被继承。对于该类的设计用不需要做任何变动,或者出于安全的考虑,你不希望它有子类。
由于final类不允许继承所以其中的方法也都隐式指定为final。
练习22:创建一个final类并试着继承它
public final class Fnl {
void service(){
System.out.println();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8nYGdXMn-1599637653667)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1592451176884.png)]
7.9 初始化及类的加载
java中所有事物都是对象,每个类的编译代码都存在于他自己的独立文件中,该文件只在需要的使用程序代码时才会被加载,一般来说,类的代码在初次使用时才加载(即创建类的第一个对象之时),但当访问static域或static方法时也会被加载(构造器也是static方法,更准确来说,类实在其任何static成员被访问时加载的)
7.9.1 继承与初始化
当你对一个导出类进行加载的时候,编译器找到他还有一个基类,继续加载它不管你是否打算产生一个该基类的对象,这都要发生。先加载基类在加载导出,其中有static先加载
练习23:请证明加载类的动作仅发生一次。证明该类的第一个实体的创建者或者对static成员的访问都有可能引起加载。
public class Test23 {
static{
System.out.println("static");
}
static void service(){
System.out.println("加载");
}
}
public class Value {
public static void main(String[] args) {
System.out.println("call");
Test23.service();
System.out.println("实例");
new Test23();
}
}
//output
call
static
加载
实例
练习24:在Beetle.java中,从Beetle类继承一个具体类型的“甲壳虫”。其形式与类相同,跟踪并解释其输出结果。
public class Jkc extends Beetle {
int m = printinit("JapaneseBeetle.m.initilized");
public Jkc() {
System.out.println("m = "+m);
System.out.println("j = "+j);
}
static int x3 = printinit("static JapansesBeetle.x3 initlized");
public static void main(String[] args) {
new Jkc();
}
}
//output
static Insect.x1 init
static Beetle.x2 init
static JapansesBeetle.x3 initlized
i= 9,j= 0
Beetle.k init
k= 47
j= 39
JapaneseBeetle.m.initilized
m = 47
j = 39
ntln(“static”);
}
static void service(){
System.out.println(“加载”);
}
}
public class Value {
public static void main(String[] args) {
System.out.println(“call”);
Test23.service();
System.out.println(“实例”);
new Test23();
}
}
//output
call
static
加载
实例
练习24:在Beetle.java中,从Beetle类继承一个具体类型的“甲壳虫”。其形式与类相同,跟踪并解释其输出结果。
```java
public class Jkc extends Beetle {
int m = printinit("JapaneseBeetle.m.initilized");
public Jkc() {
System.out.println("m = "+m);
System.out.println("j = "+j);
}
static int x3 = printinit("static JapansesBeetle.x3 initlized");
public static void main(String[] args) {
new Jkc();
}
}
//output
static Insect.x1 init
static Beetle.x2 init
static JapansesBeetle.x3 initlized
i= 9,j= 0
Beetle.k init
k= 47
j= 39
JapaneseBeetle.m.initilized
m = 47
j = 39