美文网首页
Java编程思想(五)

Java编程思想(五)

作者: MikeShine | 来源:发表于2019-04-26 15:07 被阅读0次

第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() 擦洗
*/

这里需要注意两个点:

  1. super关键字:调用父类的方法
  2. 两个类都有 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
*/
  1. 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(),所以最终输出顺序如上。

相关文章

网友评论

      本文标题:Java编程思想(五)

      本文链接:https://www.haomeiwen.com/subject/tmofnqtx.html