面向对象三个特性和七大设计原则

一、三大特性:封装、继承、多态。

  • 封装:将数据和方法包装进类中,并把具体实现隐藏起来的操作成为封装。其结果就是得到了一个同时具有特征和行为的数据类型。

  • 继承:可参考之前写的总结: java面向对象_4 _面向对象的三大特性

    • extends的意思是”扩展”。子类是父类的扩展。派生类 extends 父类。
    • JAVA中类只有单继承,没有多继承!(一个子类只能有一个父类,而一个父类可以用多个子类)
    • 子类和父类之间,从意义上讲应该具有"is a"的关系.(子类默认继承父类(public)方法)
    • super VS this关键字,this:本身调用者这个对象;super:代表父类对象的引用
    • 还涉及方法重写,父类的功能,子类不一定需要,或者不满足,就需要重写。只可重写父类的public、protected方法,private方法不具有重写特性。
  • 多态

    • 多态与继承息息相关

    • 多态,主要说的是方法的多态:具体是:

      • 一个方法,如果只有子类有,父类没有,那子类引用可以直接调用,父类引用不能直接调用(需要强转才为子类才能调用)
      • 如果一个方法,子类与父类都有,只要子类没有重写该方法,那么子类引用与父类引用就调用父类的该方法;那子类重写了该方法,子类引用与父类引用就调用的是子类里的该方法。(这里说的“子类引用与父类引用就调用的是子类里的该方法”,要注意一个前提:父类引用指向子类实例)
      • 以上这种代码的形式,就是为了代码的可扩展性。
    • 看例子:

      // polymorphism/shape/Shape.java
      package polymorphism.shape;
      public class Shape {
      	public void draw() {}
        public void erase() {}
      }
      
      派生类通过重写这些方法为每个具体的形状提供独一无二的方法行为:
      
      // polymorphism/shape/Circle.java
      
      package polymorphism.shape;
      public class Circle extends Shape {
        @Override
        public void draw() {
        	System.out.println("Circle.draw()");
        }
        @Override
        public void erase() {
        	System.out.println("Circle.erase()");
      	} 
      }
      
      // polymorphism/shape/Square.java
      package polymorphism.shape;
        public class Square extends Shape {
        @Override
        public void draw() {
        	System.out.println("Square.draw()");
        }
        @Override
        public void erase() {
        	System.out.println("Square.erase()");
        } 
      }
      
      // polymorphism/shape/Triangle.java
      package polymorphism.shape;
      
      public class Triangle extends Shape {
        @Override
        public void draw() {
        	System.out.println("Triangle.draw()");
        }
        @Override
        public void erase() {
        	System.out.println("Triangle.erase()");
        } 
      }
      
      RandomShapes 是一种工厂,每当我们调用 get() 方法时,就会产生一个指向随
      机创建的 Shape 对象的引用。注意,向上转型发生在 return 语句中,每条 return 语
      句取得一个指向某个 CircleSquareTriangle 的引用,并将其以 Shape 类型从
      get() 方法发送出去。因此无论何时调用 get() 方法,你都无法知道具体的类型是什
      么,因为你总是得到一个简单的 Shape 引用:
      
      // polymorphism/shape/RandomShapes.java
      // A "factory" that randomly creates shapes
      package polymorphism.shape;
      import java.util.*;
      public class RandomShapes {
      	private Random rand = new Random(47);
        public Shape get() {
        	switch(rand.nextInt(3)) {
        	default:
            case 0: return new Circle();
            case 1: return new Square();
            case 2: return new Triangle();
        	} 
      }
      	public Shape[] array(int sz) {
          Shape[] shapes = new Shape[sz];
          // Fill up the array with shapes:
          for (int i = 0; i < shapes.length; i++) {
            shapes[i] = get();
          }
          return shapes;
      	} 
      }
      
      array() 方法分配并填充了 Shape 数组,这里使用了 for-in 表达式:
      // polymorphism/Shapes.java
      // Polymorphism in Java
      import polymorphism.shape.*;
        public class Shapes {
          public static void main(String[] args) {
            RandomShapes gen = new RandomShapes();
            // Make polymorphic method calls:
            for (Shape shape: gen.array(9)) {
            shape.draw();
          } 
        } 
      }
      输出:
      Triangle.draw()
      Triangle.draw()
      Square.draw()
      Triangle.draw()
      Square.draw()
      Triangle.draw()
      Square.draw()
      Triangle.draw()
      Circle.draw()
        
        main() 方法中包含了一个 Shape 引用组成的数组,其中每个元素通过调用 RandomShapes 类的 get() 方法生成。现在你只知道拥有一些形状,但除此之外一无所知(编译器也是如此)。然而当遍历这个数组为每个元素调用 draw() 方法时,从运行程序
      的结果中可以看到,与类型有关的特定行为奇迹般地发生了。
      随机生成形状是为了让大家理解:在编译时,编译器不需要知道任何具体信息以进
      行正确的调用。所有对方法 draw() 的调用都是通过动态绑定进行的。
      

1.1、《On Java 8》摘抄:构造器与多态(建立在继承之上)

构造器调用顺序【*】

在 “初始化和清理” 和 “复用” 两章中已经简单地介绍过构造器的调用顺序,但那时还没有介绍多态。

在派生类的构造过程中总会调用基类的构造器。 初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。

由于属性通常声明为 private,你必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器拥有恰当的知识和权限来初始化自身的元素。因此,必须得调用所有构造器;否则就不能构造完整的对象。这就是为什么编译器会强制调用每个派生类中的构造器的原因。如果在派生类的构造器主体中没有显式地调用基类构造器,编译器就会默默地调用无参构造器。如果没有无参构造器,编译器就会报错(当类中不含构造器时,编译器会自动合成一个无参构造器)。

下面的例子展示了组合、继承和多态在构建顺序上的作用:

 // polymorphism/Sandwich.java
// Order of constructor calls
// {java polymorphism.Sandwich}
package polymorphism;
    class Meal {
        Meal() {
            System.out.println("Meal()");
        }
    }
    class Bread {
        Bread() {
            System.out.println("Bread()");
        }
    }
    class Cheese {
        Cheese() {
            System.out.println("Cheese()");
        }
    }
    class Lettuce {
        Lettuce() {
            System.out.println("Lettuce()");
        }
    }
    class Lunch extends Meal {
        Lunch() {
            System.out.println("Lunch()");
        }
    }
    class PortableLunch extends Lunch {
        PortableLunch() {
            System.out.println("PortableLunch()");
        }
    }
    public class Sandwich extends PortableLunch {
        private Bread b = new Bread();
        private Cheese c = new Cheese();
        private Lettuce l = new Lettuce();
        public Sandwich() {
            System.out.println("Sandwich()");
        }
        public static void main(String[] args) {
            new Sandwich();
        }
    } 

//输出:
    Meal()
    Lunch()
    PortableLunch()
    Bread()
    Cheese()
    Lettuce()
    Sandwich()

这个例子用其他类创建了一个复杂的类。每个类都在构造器中声明自己。重要的类是 Sandwich,它反映了三层继承(如果算上 Object 的话,就是四层),包含了三个成员对象。
从创建 Sandwich 对象的输出中可以看出对象的构造器调用顺序如下:

  1. 基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最
    先构造,然后是它的派生类,以此类推,直到最底层的派生类。
  2. 按声明顺序初始化成员。
  3. 调用派生类构造器的方法体。

构造器的调用顺序很重要。当使用继承时,就已经知道了基类的一切,并可以访问基类中任意 public 和protected 的成员。这意味着在派生类中可以假定所有的基类成员都是有效的。在一个标准方法中,构造动作已经发生过,对象其他部分的所有成员都已经创建好。

在构造器中必须确保所有的成员都已经构建完。唯一能保证这点的方法就是首先调用基类的构造器。接着,在派生类的构造器中,所有你可以访问的基类成员都已经初始化。另一个在构造器中能知道所有成员都是有效的理由是:无论何时有可能的话,你应该在所有成员对象(通过组合将对象置于类中)定义处初始化它们(例如,例子中的
b、c 和 l)。如果遵循这条实践,就可以帮助确保所有的基类成员和当前对象的成员对象都已经初始化。

不幸的是,这不能处理所有情况,在下一节会看到。

继承和清理

在使用组合和继承创建新类时,大部分时候你无需关心清理。子对象通常会留给垃圾收集器处理。如果你存在清理问题,那么必须用心地为新类创建一个 dispose() 方法(这里用的是我选择的名称,你可以使用更好的名称)。由于继承,如果有其他特殊的清理工作的话,就必须在派生类中重写 dispose() 方法。当重写 dispose() 方法时,记得调用基类的 dispose() 方法,否则基类的清理工作不会发生:

// polymorphism/Frog.java
// Cleanup and inheritance
// {java polymorphism.Frog}
package polymorphism;
    class Characteristic {
        private String s;
        Characteristic(String s) {
            this.s = s;
            System.out.println("Creating Characteristic " + s);
        }
        protected void dispose() {
            System.out.println("disposing Characteristic " + s);
        }
    }
    class Description {
        private String s;
        Description(String s) {
            this.s = s;
            System.out.println("Creating Description " + s);
        }
        protected void dispose() {
            System.out.println("disposing Description " + s);
        }
    }
    class LivingCreature {
        private Characteristic p = new Characteristic("is alive");
        private Description t = new Description("Basic Living Creature");
        LivingCreature() {
            System.out.println("LivingCreature()");
        }
        protected void dispose() {
            System.out.println("LivingCreature dispose");
            t.dispose();
            p.dispose();
        }
    }
    class Animal extends LivingCreature {
        private Characteristic p = new Characteristic("has heart");
        private Description t = new Description("Animal not Vegetable");
        Animal() {
            System.out.println("Animal()");
        }
        @Override
        protected void dispose() {
            System.out.println("Animal dispose");
            t.dispose();
            p.dispose();
            super.dispose();
        }
    }
    class Amphibian extends Animal {
        private Characteristic p = new Characteristic("can live in water");
        private Description t = new Description("Both water and land");
        Amphibian() {
            System.out.println("Amphibian()");
        }
        @Override
        protected void dispose() {
            System.out.println("Amphibian dispose");
            t.dispose();
            p.dispose();
            super.dispose();
        }
    }
    public class Frog extends Amphibian {
        private Characteristic p = new Characteristic("Croaks");
        private Description t = new Description("Eats Bugs");
        public Frog() {
            System.out.println("Frog()");
        }
        @Override
        protected void dispose() {
            System.out.println("Frog dispose");
            t.dispose();
            p.dispose();
            super.dispose();
        }
        public static void main(String[] args) {
            Frog frog = new Frog();
            System.out.println("Bye!");
            frog.dispose();
        }
    }
//输出:
Creating Characteristic is alive
Creating Description Basic Living Creature
LivingCreature()
Creating Characteristiv has heart
Creating Description Animal not Vegetable
Animal()
Creating Characteristic can live in water
Creating Description Both water and land
Amphibian()
Creating Characteristic Croaks
Creating Description Eats Bugs
Frog()
Bye!
Frog dispose
disposing Description Eats Bugs
disposing Characteristic Croaks
Amphibian dispose
disposing Description Both wanter and land
disposing Characteristic can live in water
Animal dispose
disposing Description Animal not Vegetable
disposing Characteristic has heart
LivingCreature dispose
disposing Description Basic Living Creature
disposing Characteristic is alive

层级结构中的每个类都有 Characteristic 和 Description 两个类型的成员对象,它们必须得被销毁。销毁的顺序应该与初始化的顺序相反,以防一个对象依赖另一个对象。 对于属性来说,就意味着与声明的顺序相反(因为属性是按照声明顺序初始化的)。 对于基类(遵循 C++ 析构函数的形式),首先进行派生类的清理工作,然后才是基类的清理。这是因为派生类的清理可能调用基类的一些方法,所以基类组件这时得存活,不能过早地被销毁。输出显示了,Frog 对象的所有部分都是按照创建的逆序销毁的。

尽管通常不必进行清理工作,但万一需要时,就得谨慎小心地执行。

Frog 对象拥有自己的成员对象,它创建了这些成员对象,并且知道它们能存活多久,所以它知道何时调用 dispose() 方法。然而,一旦某个成员对象被其它一个或多个对象共享时,问题就变得复杂了,不能只是简单地调用 dispose()。这里,也许就必须使用引用计数来跟踪仍然访问着共享对象的对象数量,如下:

 // polymorphism/ReferenceCounting.java
// Cleaning up shared member objects
    class Shared {
        private int refcount = 0;
        private static long counter = 0;
        private final long id = counter++;
        Shared() {
            System.out.println("Creating " + this);
        }
        public void addRef() {
            refcount++;
        }
        protected void dispose() {
            if (--refcount == 0) {
                System.out.println("Disposing " + this);
            }
        }
        @Override
        public String toString() {
            return "Shared " + id;
        }
    }
    class Composing {
        private Shared shared;
        private static long counter = 0;
        private final long id = counter++;
        Composing(Shared shared) {
            System.out.println("Creating " + this);
            this.shared = shared;
            this.shared.addRef();
        }
        protected void dispose() {
            System.out.println("disposing " + this);
            shared.dispose();
        }
        @Override
        public String toString() {
            return "Composing " + id;
        }
    }
    public class ReferenceCounting {
        public static void main(String[] args) {
            Shared shared = new Shared();
            Composing[] composing = {
                    new Composing(shared),
                    new Composing(shared),
                    new Composing(shared),
                    new Composing(shared),
                    new Composing(shared),
            };
            for (Composing c: composing) {
                c.dispose();
            }
        }
    }
//输出:
Creating Shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
Creating Composing 4
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared 0

static long counter 跟踪所创建的 Shared 实例数量,还提供了 id 的值。counter的类型是 long 而不是 int,以防溢出(这只是个良好实践,对于本书的所有示例,counter不会溢出)。id 是 final 的,因为它的值在初始化时确定后不应该变化。

在将一个 shared 对象附着在类上时,必须记住调用 addRef(),而 dispose() 方法会跟踪引用数,以确定在何时真正地执行清理工作。使用这种技巧需要加倍细心,但是如果正在共享需要被清理的对象,就没有太多选择了。

构造器内部多态方法的行为【*】

构造器调用的层次结构带来了一个困境。如果在构造器中调用了正在构造的对象的动态绑定方法,会发生什么呢?

在普通的方法中,动态绑定的调用是在运行时解析的,因为对象不知道它属于方法所在的类还是类的派生类。

如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些bug 很隐蔽,难以发现。

从概念上讲,构造器的工作就是创建对象(这并非是平常的工作)。在构造器内部,整个对象可能只是部分形成——只知道基类对象已经初始化。如果构造器只是构造对象过程中的一个步骤,且构造的对象所属的类是从构造器所属的类派生出的,那么派生部分在当前构造器被调用时还没有初始化。然而,一个动态绑定的方法调用向外深入到继承层次结构中,它可以调用派生类的方法。如果你在构造器中这么做,就可能调用一个方法,该方法操纵的成员可能还没有初始化——这肯定会带来灾难。

下面例子展示了这个问题:

 // polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect
    class Glyph {
        void draw() {
            System.out.println("Glyph.draw()");
        }
        Glyph() {
            System.out.println("Glyph() before draw()");
            draw();
            System.out.println("Glyph() after draw()");
        }
    }
    class RoundGlyph extends Glyph {
        private int radius = 1;
        RoundGlyph(int r) {
            radius = r;
            System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
        }
        @Override
        void draw() {
            System.out.println("RoundGlyph.draw(), radius = " + radius);
        }
    }
    public class PolyConstructors {
        public static void main(String[] args) {
            new RoundGlyph(5);
        }
    }
//输出:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
    

Glyph 的 draw() 被设计为可重写,在 RoundGlyph 这个方法被重写。但是 Glyph的构造器里调用了这个方法,结果调用了 RoundGlyph 的 draw() 方法,这看起来正是我们的目的。输出结果表明,当 Glyph 构造器调用了 draw() 时,radius 的值不是默认初始值 1 而是 0。这可能会导致在屏幕上只画了一个点或干脆什么都不画,于是我们只能干瞪眼,试图找到程序不工作的原因。

前一小节描述的初始化顺序并不十分完整,而这正是解决谜团的关键所在。初始化的实际过程是:

  1. 在所有事发生前,分配给对象的存储空间会被初始化为二进制 0。
  2. 如前所述调用基类构造器。此时调用重写后的 draw() 方法(是的,在调用 RoundGraph 构造器之前调用),由步骤 1 可知,radius 的值为 0。
  3. 按声明顺序初始化成员。
  4. 最终调用派生类的构造器。

这么做有个优点:所有事物至少初始化为 0(或某些特殊数据类型与 0 等价的值),而不是仅仅留作垃圾。这包括了通过组合嵌入类中的对象引用,被赋予 null。如果忘记初始化该引用,就会在运行时出现异常。观察输出结果,就会发现所有事物都是 0。

另一方面,应该震惊于输出结果。逻辑方面我们已经做得非常完美,然而行为仍不可思议的错了,编译器也没有报错(C++ 在这种情况下会产生更加合理的行为)。像这样的 bug 很容易被忽略,需要花很长时间才能发现。

因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的final 方法(这也适用于可被看作是 final 的 private 方法)。这些方法不能被重写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力。

二、OOP七大设计原则

一、开闭原则:对扩展开放,对修改关闭

  • 讲的是设计要对扩展有好的支持,而对修改要严格限制。即对扩展开放,对修改封闭。
  • 优点:降低了程序各部分之间的耦合性,其适应性、灵活性、稳定性都比较好。当已有软件系统需要增加新的功能时,不需要对作为系统基础的抽象层进行修改,只需要在原有基础上附加新的模块就能实现所需要添加的功能。增加的新模块对原有的模块完全没有影响或影响很小,这样就无须为原有模块进行重新测试。

二、里氏替换原则:继承必须确保超类所拥有的性质在子类中仍然成立(子类可以改变父类中的功能,但是尽量不要修改父类中原有的功能)

  • 很严格的原则,规则是“子类必须能够替换基类,否则不应当设计为其子类。”也就是说,一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化。
  • 优点:可以很容易的实现同一父类下各个子类的互换,而客户端可以毫不察觉。

三、依赖倒置原则:要面向接口编程,不要面向实现编程。

  • “设计要依赖于抽象而不是具体化”。换句话说就是设计的时候我们要用抽象来思考,而不是一上来就开始划分我需要哪些哪些类,因为这些是具体。
  • 高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。另一种表述为: 要针对接口编程,不要针对实现编程。即“Program to an interface, not an implementation.”
  • 优点:人的思维本身实际上就是很抽象的,我们分析问题的时候不是一下子就考虑到细节,而是很抽象的将整个问题都构思出来,所以面向抽象设计是符合人的思维的。另外这个原则会很好的支持(开闭原则)OCP,面向抽象的设计使我们能够不必太多依赖于实现,这样扩展就成为了可能,这个原则也是另一篇文章《Design by Contract》的基石。

四、单一职责原则:控制类的粒度大小、将对象解耦、提高其内聚性。(一个方法尽量只做一件事情,类职责单一)

  • 一个合理的类,应该仅有一个引起它变化的原因,即单一职责,就是设计的这个类功能应该只有一个;
  • 优点:消除耦合,减小因需求变化引起代码僵化。

五、接口隔离原则:要为各个类建立它们需要的专用接口

  • “将大的接口打散成多个小接口”,让系统解耦,从而容易重构,更改和重新部署。

  • 优点:会使一个软件系统功能扩展时,修改的压力不会传到别的对象那里。

六、迪米特法则:只与你的直接朋友交谈,不跟“陌生人"说话。(A<->B<->C,A和C之间不要互相沟通,降低类之间的耦合度)

  • 迪米特法则或最少知识原则,这个原则首次在Demeter系统中得到正式运用,所以定义为迪米特法则。它讲的是“一个对象应当尽可能少的去了解其他对象”。
  • 优点:消除耦合。

七、合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
面向对象八大设计原则是一组指导面向对象软件设计的原则,也被称为SOLID原则。这些原则旨在提高软件的可维护性、可扩展性和可重用性。下面是对这八大设计原则的介绍: 1. 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责。 2. 开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改现有代码的情况下,可以通过添加新的代码来扩展功能。 3. 里氏替换原则(Liskov Substitution Principle,LSP):子类型必须能够替换掉它们的父类型。也就是说,如果一个类是父类的子类,那么它可以在任何使用父类的地方替代父类而不会引发错误。 4. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖它不需要的接口。一个类不应该强迫它的客户端依赖于它们不使用的方法。 5. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。 6. 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解。一个类应该只与它的直接朋友进行通信,而不是与陌生的类进行通信。 7. 合成复用原则(Composite Reuse Principle,CRP):尽量使用对象组合而不是继承来达到复用的目的。通过将对象组合在一起,可以灵活地增加新的行为,而不需要修改现有的代码。 8. 优先使用组合而不是继承原则(Prefer Composition Over Inheritance):在设计时,应优先考虑使用对象组合来实现代码的复用和扩展性,而不是过度使用继承。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬浮海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值