美文网首页
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