Java基础系列二、封装继承多态

1、对象内存空间分布图

  ① 、每创建一个对象都会在堆内存中开辟一块空间,并且这块空间中具有和类(模板)中一样的成员。

  ② 、每一个对象都被栈中的一个变量所指向,所以操作栈中的变量(s)就如同操作堆中的对象。

  ③ 、s.name = "小王";其实是把字符串值赋值给s变量指向的堆中的name字段上的,而不是设置给类的,所以我们在分析代码的时候,看到new Student()对象应该立马想到在堆中有一个对象。

2、构造方法要点

  1、构造方法名与类的名字一模一样
  2、没有返回值类型修饰

  3、 构造方法可以创建对象。
  4、因为构造方法的调用是在 对象数据区 赋值工作 之前执行的,所以 构造方法常用来给对象的属性进行 初始化 工作
  5、给对象开辟内存空间时会自动调用
  6、当类中没有自定义有参数构造方法和无参数构造方法时,程序中会默认隐藏一个 无参构造方法 ( public className(){ } ) 

   当程序中自定义了一个有参数数构造方法或者无参数构造方时,那么隐藏的那个无参构造方法就被覆盖(不存在了)

  7、构造方法可以重载,不可以重写。因为构造方法不能继承到子类。

3、匿名对象  

 概念: 没有名字的对象,创建对象时没有对应类型的变量去接收。

 优点
       匿名对象只能使用一次, 匿名对象调用完毕就是垃圾,可以被垃圾回收器回收,提高内存使用效率。
       匿名对象可以作为实际参数传递。

package cn.manman.com;
/*
 * 匿名对象的应用场景:
 *  A:调用方法,仅仅只调用一次的时候;
 *  优势是:匿名对象调用完就被垃圾回收器回收。提高内存使用效率;
 *  B:匿名对象可以作为实际参数传递;
 *  
 */
public class NoNameDemo {
    public static void main(String[] args) {
        //带名字的调用
        Student student=new Student();
        student.show();
        student.show();//这里的对象和上一个对象是同一个对象;
        //匿名对象
        new Student().eat();
        new Student().eat();//这里其实是重新创建了一个新的对象在调用
    }
}
class Student{
    public void show(){
        System.out.println("我们爱学习!");
    }
    public void eat(){
        System.out.println("爱吃火锅!");
    }
}

  

4、对象的生命周期

  开始:new的时候就开始了; 

  结束(说法1,常见的说法) :当对象失去所有的引用(没有变量再指向它了(没有栈空间的变量去在存储它在堆空间的地址)- 相当于失联了,我们无法再使用它了)-- 就是死亡了;(垃圾回收器 并不是立刻进行回收)

  结束(说法2) : 对象真正的被销毁(对象会在堆里面占用内存,当把对象的内存空间回收了),Java有自动垃圾回收机制;

5、值传递和引用传递的理解

先看案例:

public class StringBase {
 
    public static void main(String[] args) {
        int c = 66; //c 叫做实参
        String d = "hello"; //d 叫做实参
 
        StringBase stringBase = new StringBase();
        stringBase.test5(c, d); // 此处 c 与 d 叫做实参
 
        System.out.println("c的值是:" + c + " --- d的值是:" + d);
    }
    
    public void test5(int a, String b) { // a 与 b 叫做形参
        a = 55;
        b = "no";
    }
}

运行结果: c的值是:66 --- d的值是:hello

1、值传递

    在方法的调用过程中,实参把它的实际值传递给形参,此传递过程就是将实参的值复制一份传递到函数中,这样如果在函数中对该值(形参的值)进行了操作将不会影响实参的值。因为是直接复制,所以这种方式在传递大量数据时,运行效率会特别低下。

2、引用传递

    引用传递弥补了值传递的不足,如果传递的数据量很大,直接复过去的话,会占用大量的内存空间,而引用传递就是将对象的地址值传递过去,函数接收的是原始值的首地址值。在方法的执行过程中,形参和实参的内容相同,指向同一块内存地址,也就是说操作的其实都是源数据,所以方法的执行将会影响到实际对象。

面试的时候简单描述:

  1. 基本数据类型传值,对形参的修改不会影响实参;
  2. 引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象。
  3. String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。

6、静态修饰符static

  1、可以去修饰 成员变量(属性或者字段),不可以修饰 类(外部类),构造方法,局部变量

  2、有static修饰的字段和方法,我们可以用字段所在的类的 类名.字段  类名.方法去访问,没有用static修饰的字段和方法,只能用实例去访问它,即创建对象去调用。

注意:
  1、静态的属性放在 类的数据区 内, 所以 每个对象访问静态属性的时候 都是去 类的数据区中访问的静态属性的值
  2、对象的数据区 中不包含静态属性。
  3、一般情况下,类中的全局常量 用static修饰的较多,而类中的 字段 很少使用static修饰

  static修饰的属性和方法在什么时候加载到类数据区里?
  当程序编译时,该类中用到的所有类,这些类都会编译,然后这些的静态属性和静态方法都会被加载到类的数据区里去,看下图:

7、访问权限修饰符   

作用:主要用来修饰类中的成员(属性,普通方法,构造方法),也可以去修饰类

  private :该修饰符只能在本类中访问
  default(缺省不写):在不同包下面不可以访问
  protected:在不同包下面不可以访问
  public:在任何地方都可以访问
注意:
  1、public 、默认的(缺省不写) 这两种可以去修饰类(外部类,内部类)
  2、private 和protected 不能去修饰外部类
  3、所有的访问权限修饰符都不可以去修饰 局部变量
总结:
   公共的修饰(public):成员方法  字段(可以去修饰但是一般情况下用private修饰),构造方法, 内部类 ,类
  私有的修饰(private): 字段,,成员方法(一帮情况成员方法用public修饰),构造方法 ,内部类

  

8、javabean是一个标准的java类   

  要求:
    1.类必须要是public修饰的
    2.类中的属性必须是private修饰的
    3.类中必须提供一个无参数构造器
    4.每个私有化的属性必须提供一组getter/setter方法

  注意: 1.Boolean类型(布尔的包装类),生成的get方法是get开头的(建议使用这个). 
      2.boolean类型,生成的get方法是is开头的   (用这个最好重写 getXxx() 格式的方法,因为涉及到反射,反射一般会默认调取对象的get方法) 

9、理解this关键字

  用法:
    1、在方法中出现的this代指调用该方法的对象
    2、this可以解决属性与局部变量同名时的冲突

Public Class Student { 
   String name; //定义一个成员变量name
   private void SetName(String name) { //定义一个参数(局部变量)name
      this.name=name; //将局部变量的值传递给成员变量
   }
}

  

  

  3、this可以调用构造方法(注意:this必须是构造器中的第一条语句)

public class Student { //定义一个类,类的名字为student。 
     public Student() { //定义一个方法,名字与类相同故为构造方法
        this(“Hello!”);
     }
     public Student(String name) { //定义一个带形式参数的构造方法
     }
}

  4、this可以作为方法的返回值返回的是调用this所处方法的那个对象的引用,更简单点说,就是谁调用返回的就是谁。

  由于返回的是对象引用,所以this不能用在静态成员方法中,只能在非静态成员方法中出现。

  详细参考:https://www.cnblogs.com/chanchan/p/7812166.html

  5、this可以作为方法的调用时的实参
  6、this在自定义类型中的使用(同桌问题,一定要理解)

 class People {
      private String name;
      private People friend;
      public void setName(String name){
           this.name = name;
      }
      public String getName(){
          return this.name;
      }
     public void setFriend(People friend){
          if(this.getFriend() == friend){
             return;
          }
          this.friend = friend;
          friend.setFriend(this);//此种方式注意死循环
     }
     public People getFriend(){
         return this.friend;
     }
 }
 
 public class TestPeople {
     public static void main(String[] args) {
         People p1 = new People();
         p1.setName("小王");
         
         People p2 = new People();
         p2.setName("小张");
 
         //设置朋友关系
         p1.setFriend(p2);
         System.out.println(p1.getName()+"的朋友是:"+p1.getFriend().getName());
         //p2.setFriend(p1);
         System.out.println(p2.getName()+"的朋友是:"+p2.getFriend().getName());
     }
 }

  

10、方法的重写、方法的重载

  方法的重写概念:将父类的方法复制到子类重新定义方法体内的代码的过程,子类中拥有一个和父类完全一样的方法,将这两个方法称为重写。

  方法重写的要求:

      1、保证子类方法和父类方法的方法签名(方法名+参数列表)一致,其中形参的名称是否一样无所谓

      2、(访问权限等级高低   public >  默认的  > private)  子类的方法的访问修饰符的等级  等于或者大于   父类的方法的访问修饰符

      3、private修饰的方法不能被重写(private修饰的成员方法不能被继承到子类)

      4、static修饰的方法不能被重写(static修饰的方法存在类的数据区,不在对象数据区)

      5、子类中重写的方法的返回值类型 与 父类方法返回值类型 相同或者是父类的返回值类型的子类(不是java中的等级高低)

  注意:

      1、在编译阶段验证是否覆写: 在子类方法上面加 @Override ,用来检测子类中的方法是否重写父类的方法,让编译器来检查,如果是正确的覆写,编译通过,否则编译报错

      2、子类方法和父类方法完全一样,也是方法的重写。

  方法的重载概念: 在同一个类中方法名必须相同,形参列表中的顺序,个数,类型不同的两个或者两个以上的方法称为方法的重载,且与返回值类型、访问修饰符无关。

 

 11、Object类、toString()、equals()介绍

注意:★(所有的引用数据类型的对象都属于Object类型)

  1、Object类位于java.lang包下面,使用时不需要导包

  2、所有的引用数据类型的对象都可以使用Object类里的方法

  3、基本数据类型变量 不可以使用Object类里的方法

toString()

  哪个对象调用toString()方法,该方法就返回该对象的字符串表示形式

  如果对象所属的类中没有重写toString,那么对象访问的是Objet类中toString方法

  如果对象所属的类中重写了toString ,那么对象访问的是他自己本身类中重写的toString方法

  注意:在自定义类中重写toString()的方法是因为打印对象名时,不想看到类似这样"Student@56dd8cc"的值,而是想清楚的知道对象的属性值是什么。

  1、Object 和String 中的toString()的源码

//   Object 中的toString();
    public String toString() {
         return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
 //   String 中的toString();
     public String toString() {
         return this;
     }

equals()

  2、Object中的equals怎么定义的?

//lang包--   Object.java  中的equals();
     public boolean equals(Object obj) {
         return (this == obj);
     }

   3、String 中的equals怎么定义的?

//lang包--  String.java 中的equals();
     public boolean equals(Object anObject) {
        if (this == anObject) {
             return true;
         }
       if (anObject instanceof String) {
             //强制转换,装箱。
            String anotherString = (String)anObject;
//            value指前面的需要比较的字符串
           int n = value.length;
             if (n == anotherString.value.length) {
                char v1[] = value;
                 char v2[] = anotherString.value;
                 int i = 0;
                 while (n-- != 0) {
                     if (v1[i] != v2[i])
                         return false;
                     i++;
                }
                 return true;
             }
         }
         return false;
     }

  4、自定义类中的equals方法怎么定义?

 //自定义类中的equals方法
     public boolean equals(Student stu) {
          if(((this.getName()).equals(stu.getName())) && ((this.getStuId())==(stu.getStuId()))){
              return true;
          }
          return false;
      }
  /*    1.编译器看到的obj的类型是  Object ,Object中是没有name和age的,编译报错
      但是,我们知道实际调用的时候obj变量中装的是一个学生对象,
     应该明确的告诉编译器这个obj是学生对象---》 obj强制转换成 Student*/
 /*    2.this.name  其实类型是String 是引用类型(非常特殊的引用类型) ; 
     使用== 比较有风险(可能比较的是地址),应该比较字符串的字面值
     String类在设计的时候就于已经覆写了Object中的equals方法,比较规则就是使用的字面值进行比较
     所以 上面this.name == s.getName()    应该调用String类中的equals方法*/
     public boolean equals(Object obj) {
         Student s = (Student)obj;
         if(this.name.equals(s.getName())&&this.age == s.getAge()){
             return true;
         }
         return false;
     }
 /*    3.上面的代码 if中的条件 本身就是一个逻辑运算,逻辑运算表达式的结果值就是一个boolean
     所以没有必要写if   else*/
     public boolean equals(Object obj) {
         Student s = (Student)obj;
         return this.name.equals(s.getName())&&this.age == s.getAge();
     }

  ★★:5、为什么调用println方法会  自动调用toString方法?

 //io包--PrintStream.java 里面的println();
        public void println(Object x) {
          String s = String.valueOf(x);
          synchronized (this) {
              print(s);
              newLine();
          }
      }
      
//lang包--String.java 里面的valueOf();
     public static String valueOf(Object obj) {
         return (obj == null) ? "null" : obj.toString();
     }

 ★★★★★6、== equals 都是比较是否相等,请问它们到底有什么区别呢?

      1 、==

基本数据类型:    比较的就是值是否相等;

       引用数据类型:    比较的是对象的地址是否一样;(排除特殊 String

  2 、equals  (最初定义在根类Object中的)

基本数据类型 :  不能够使用!   基本数据类型不是对象,不能够调用Object中的方法

引用数据类型 :  Object的源码中定义的就是==进行比较比较,比较的是对象的地址是否一样

         而在String类型和包装类型(Integer)都重写了equals方法,比较是对应的值。

在实际开发中,我们一般比较对象都是通过对象的属性值进行比较(一般比较对象的地址没有多大用处),所以我们会经常覆写Object中的此方法,把自己的规则写在方法里面;

12、super关键字

  作用:调用父类中的成员(属性,成员方法,构造方法)

  1、在子类的方法中使用,代指父类对象,可以去调用父类属性和方法

  2、在子类的构造方法 里访问父类的构造方法  用super() 调用无参父类构造方法。用super( 参数列表) 调用有参父类构造方法。

  3、子类的构造器中的第一行代码会默认隐藏一个 super() ,当子类构造器中书写了super()或者this()的 (有参无参 )时,构造器中的隐藏的super()会被覆盖。

      4、super() 和 this() 表达式 必须是构造器中的第一条语句,导致了super(),this()不能在构造器中同时使用,
    
使用场景:
  1、super可以去访问父类中的非私有化的成员
  2、调用父类构造方法(在子类构造器中 对父类私有化的属性进行初始化值)
  3、子类构造器中第一条语句默认存在super();

13、final 关键字

  final 修饰的类不能有子类,即不能被继承。

  final 不能修饰构造方法,final修饰的方法不能被重写。

  final 修饰的常量要初始化,且不能更改。(final修饰的常量要么直接赋值,要么使用有参或者无参的构造方法进行初始化,使用setter方法赋值会报错)

  final 修饰的对象的引用不能改变。列如数组和对象的变量引用。

14、封装 

  思想:就是把对象的属性和行为(方法)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,对外只提供能使用的方式(getter(),setter(),起到了安全性的作用。

15、继承

子类继承父类中某些成员  ,  父类(超类,基类,根类),子类(派生类,拓展类)

  泛化:在多个子类的基础上面抽取共有的属性和行为到一个父类中去。

  特化:在一个父类 的基础上拓展子类的特有属性和行为,生成一个新的子类。

  原则:父类中存在共性,子类中存在特性。  

  优势:  提高代码的复用性。  

  子类可以去继承哪些成员?1、非私有化的成员(字段,方法) 2、private修饰的成员以及构造方法不能被继承

继承的特点
  1、在java程序中继承只允许单继承(一个子类只允许有一个直接父类)
  2、在java程序中允许每个类之间多重继承 eg: A extends B, B extends C C extends D
  3、如果类都没有去直接继承另外一个类,那么该类会默认继承 超级基类(Object类)

16、多态

  1、概念:一种事物(对象)有多种形态(类型)可以屏蔽不同的子类之间的实现差异,多态是指通过指向父类的指针,来调用在不同子类中实现的方法。

    多态形式创建对象:
      定义类型  对象名  = new 实际类型( );
      定义类型:父类, 实际类型是:子类  ,也就是定义了一个 指向子类 的 父类引用类型 的 对象

  2、多态方法调用编译和运行时的过程。

  上面两句代码的编译,运行过程:

  编译 :  第三行, 如果AnimalPerson的父类,那么编译通过,否则编译报错;

           第四行, (编译器把p1看成是Animal) 编译的时候会到p1的编译类型中找是否有eat方法,如果没有,会继续向p1的编译类型的父类中一直向上找,如果都没有找到,编译报错, 如果找到了编译通过

        (不会向下到子类中去找找的时候就是编译的时候是不会执行代码的)

  运行 : 第四行 , 先到运行时类型(Person)eat方法,如果找到就执行,否则就向上到父类中找并执行

 注意:

  1、多态形式创建的对象可以调用哪些属性和方法,取决于定义类型

  2、多态形式创建的对象的实际类型(子类)中,方法发生了重写,那么该对象调用的是子类中的方法

  实际开发中一般不会在子类中定义一个和父类同名的字段,如果是有这样的情况存在,如果是使用的父类的对象,取值是父类里面的值; 如果子类对象,取值是子类里面的值;

  也就是父类类型的引用可以调用父类中定义的所有属性和方法

  

17、引用类型转换   

  小转大(向上转型):将子类对象 赋值给 父类对象的变量保存的过程,此时自动转换

  大转小(向下转型):将父类对象 赋值给 子类对象的变量的过程,但此时需要强转

  注意一般在开发中遇到向下转型时,都会对该对象的实际类型进行判断

  boolean is = obj(目标对象) instanceof Type(目标对象类型)

18、抽象类和接口

  理解:   我们都知道在面向对象的领域一切都是对象,同时所有的对象都是通过类来描述的,但是并不是所有的类都是来描述对象的。如果一个类没有足够的信息来描述一个具体的对象,而需要其他具体的类来支撑它,那么这样的类我们称它为抽象类。

  1、使用abstract修饰的类是抽象类,抽象类本质也是一个类

  2、类中有的成员  抽象类都可以有(字段 方法  构造方法),此外抽象类还比普通类多一个抽象方法, 故抽象类不允许创建对象。当然,抽象类中可以没有抽象方法。

  3、抽象方法:用abstract修饰的方法 ,它没有方法体,并且定义时最后结束加;而且抽象方法 必须存于抽象类中(接口也可以),不能够放在普通类中。

  4、一般将抽象的类作为父类, (普通)子类继承抽象父类,必须重写所有父类中的抽象方法。当然,如果子类也是抽象类,可以不用去重写父类中的抽象方法。

注意:abstarct 不能修饰属性,因为属性没有必要实现。

   接口引用指向实现类的对象  比如:List list = new ArrayList();

  接口  理解:接口本身就不是类, 接口是抽象类的延伸,接口是用来建立类与类之间的协议,它只提供一种形式,而没有具体的实现。

       同时实现该接口的实现类  必须  要 实现该接口的所有方法,通过使用implements关键字,他表示该类在遵循 某个或 某组 特定的接口。

       java为了保证数据安全是不能多重继承的,也就是说继承只能存在一个父类,但是接口不同,一个类可以同时实现多个接口,

      不管这些接口之间有没有关系,所以接口弥补了抽象类不能多重继承的缺陷,但是推荐继承和接口共同使用,因为这样既可以保证数据安全性又可以实现多重继承。

   接口定义: interface 接口名{}

    接口内部可以有哪些成员--参考类

      字段   全部都是全局常量(public static final修饰), 故可以直接调用

      方法   全部都是抽象方法(缺省修饰 public abstract)(接口中可以没有抽象方法,但是没意义)

         抽象方法需要子类类覆写才有意义,而static final修饰的方法都不能够被覆写,接口中的方法不可以用 static ,final修饰

       接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法; 构造方法  没有!

    注意

      1.接口主要强调的是功能,必须要有实现类去实现(实现的时候注意public修饰符)

      2.接口相当于实现类的父类(多态时体现),类可以去实现多个接口,类与类单继承,一个接口可以去继承多个接口,但接口不可以去实现接口 。

  接口和抽象的区别:
    1.接口概念
    2.抽象概念

    3. 相同点:  1. 抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。

              2. 抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类还只能是抽象类。

            同样,一个类实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。


    4. 不同点  

       1. 抽象层次不同。抽象类是对类抽象,而接口是对行为的抽象。即  抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

       2. 跨域不同。抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。

             例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞Fly接口,具备飞的行为,这里我们总不能将鸟、飞机共用一个父类吧!

             所以说抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is-a" 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已。

       3.设计层次不同。对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。比如我们只有一个猫类在这里,如果你这是就抽象成一个动物类,是不是设计有点儿过度?我们起码要有两个动物类,猫、狗在这里,我们在抽象他们的共同点,形成动物抽象类吧!所以说抽象类往往都是通过重构而来的!但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。 

19、谈谈你对面向对象的理解

         面向对象是向现实世界模型的自然延伸,这是一种“万物皆对象”的编程思想。在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。

  面向对象的编程是以对象为中心,以 消息为驱动,所以程序=对象+消息。

    面向对象有三大特性,封装、继承和多态。

    封装就是将一类事物的属性和行为抽象成一个类,使其属性私有化,行为公开化,提高了数据的隐秘性的同时,使代码模块化。这样做使得代码的复用性更高。

      继承则是进一步将一类事物共有的属性和行为抽象成一个父类,而每一个子类是一个特殊的父类--有父类的行为和属性,也有自己特有的行为和属性。这样做扩展了已存在的代码块,进一步提高了代码的复用性。

    如果说封装和继承是为了使代码重用,那么多态则是为了实现接口重用。多态的一大作用就是为了解耦--为了解除父子类继承的耦合度。如果说继承中父子类的关系式IS-A的关系,那么接口和实现类之之间的关系式HAS-A。简单来说,多态就是允许父类引用(或接口)指向子类(或实现类)对象。很多的设计模式都是基于面向对象的多态性设计的。

总结一下,如果说封装和继承是面向对象的基础,那么多态则是面向对象最精髓的理论。掌握多态必先了解接口,只有充分理解接口才能更好的应用多态。

20、面向对象和面向过程的区别?

  做菜为例,其实面向过程就好像你是个厨师,要自己炒菜,所以要讲究步骤,

  而面向对象就好像你是个食客,你只要通知厨师作菜,即发一个消息就可以了,至于厨师怎样作菜,是不用知道的。
  ---------------------------------------------------

  两句话:

  面向过程是一种以事件为中心的编程思想。就是分析出解决问题所需的步骤,然后用函数把这些步骤实现,并按顺序调用。(一种自顶向下的编程。

  面向对象是以“对象”为中心的编程思想。(自下向上先建立抽象模型然后再使用模型)。

21、面向对象开发的六个基本原则,迪米特法则

  单一职责:一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不设计与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。

  开放封闭:软件实体应当对扩展开放,对修改关闭。要做到开闭有两个要点。第一、抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;第二、封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而混乱。

  里式替换:任何时候都可以用子类型替换掉父类型。子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。

  依赖倒置:面向接口编程(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体的类型,因为抽象类型可以被它的任何一个子类型所替代)。

  合成聚合复用:优先使用聚合或合成关系复用的代码。

  接口隔离:接口要小而专,绝不能大而全。臃肿的接口是对接口的污染。既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高内聚的。

  迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。

  项目中用到的原则:单一职责、开放封闭、合成聚合复用(最简单的例子就是String类)、接口隔离。

22、枚举

   当我们遇到  属性值是固定值个数(属性值有确定范围)正常来可以使用单例模式,我们在类中使用单例模式需要创建本类的对象,当我们声明多个对象时,public static final及构造器就是重复性的代码,而枚举简化了创建本类对象时的格式,从而简化了单例模式的声明的方式。即枚举能够解决属性值是固定个数的问题

看下面代码使用单利模式解决属性值确定范围的问题。显然重复性代码居多。 

// 以知人的性别只有男女之分,固属性值固定。
public class Gender {
    private Gender(){}
    
    private static Gender man = new Gender();
    private static Gender woman = new Gender();
    
//外部通过方法获取私有的对象    
    public static Gender getMan() {
        return man;
    }
    public static Gender getWoman() {
        return woman;
    }

//重写toString方法
     public String toString(){
         if(this == man){
             return "男";
         }else if(this == woman){
            return  "女";
         }
        return null;
     }
}
public class Student {
    private Gender gender;

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }
    

}
public class Test {

    public static void main(String[] args) {
        Student s = new Student();
        
        s.setGender(Gender.getMan());
        //打印对象
        System.out.println(s.getGender());  //男
    }

}

看下面代码使用 枚举 解决 季节这个属性值固定问题

 public enum Season {
      spring("春",1),summer("夏",2),autumn(),winter("冬",4);
      
      private String name = "哈哈"; //初始值
      private int index;
      
     public String getName() {
         return name;
    }
     public void setName(String name) {
         this.name = name;
     }
     public int getIndex() {
         return index;
     }
     public void setIndex(int index) {
         this.index = index;
     }
 // 私有的有参的构造方法
     private Season(String name,int index){
         this.name = name;
         this.index = index;
     }
 //私有的无参构造方法  ,这样枚举对象里面可以不用传值。
     private Season(){}
//重写toString();
     public String toString(){
          return this.name+""+this.index;
      }
 }
 public class Test {
      public static void main(String[] args) {
          Season s =  Season.spring;   //通过枚举类名.对象名访问 固定不变的属性(对象)
          System.out.println(s);
         
         Season[] ss = Season.values();//获取枚举类中定义的所有的对象,返回一个枚举类型的对象数组
         for (Season i : ss) {
             System.out.println(i.getName()+","+i.getIndex());
         }
     }
  }
结果为:
  春1
  春,1
  夏,2
  哈哈,0
  冬,4

   

23、组合关系

  ①、组合是(has-a)关系,而继承则是(is-a)关系;

  ②、 组合关系在运行期决定,而继承关系在编译期就已经决定了。

  ③、 组合是在组合类和被包含类之间的一种松耦合关系,而继承则是父类和子类之间的一种紧耦合关系。

  ④ 、当选择使用组合关系时,在组合类中包含了外部类的对象,组合类可以调用外部类必须的方法,而使用继承关系时,父类的所有方法和变量都被子类无条件继承,子类不能选择。

24、单例模式 (介绍五种)  饿汉模式   懒汉模式  双重检测锁模式   枚举   静态内部类模式

  单例类必须自己创建自己唯一的实例(对象)

1、饿汉模式

  //饿汉模式
  public class Student {
  //私有化无参的构造方法,其他类就不可以随便创建对象了。
      private Student(){};
  //私有的,静态的对象,创建一次地址就不会改变,必须通过方法来访问
      private static Student s = new Student();
  //静态的方法,外部直接使用  类名.方法 调用
      public static Student getInstance(){
          return s;
     }
 }

2、懒汉模式

 //懒汉模式
  public class Teacher {
      private Teacher(){};    
      private static Teacher t = null;
      public static Teacher getInstance(){
  //if语句为了 防止在堆内存空间创建不同的对象 而 违反了单例原则
          if(t == null){
             t = new Teacher();
         }
         return t;
     }
 }

二者比较:

  1 单例模式的类也是一个普通的类,其中也可以有其他的字段  方法等。。。

  2 上面代码中,s对象是Student类被加载(把类放到JVM的过程中)的时候创建的

  3 如果Student类中其他的字段和方法很多。。。,创建对象的过程比较长,类加载会比较慢

   有可能加载之后很长时间其实都没有人来获得对象,浪费堆内存空间,这就是饿汉模式

  4 在类加载的时候先不创建对象,而是在有人第一次来调用方法获得对象的时候才创建一个对象

   之后需要保存起来,以后再有人调用就不用创建对象。这就是懒汉模式

总结:
  饿汉模式,类加载的时候效率低,获取单例对象时效率高,线程安全
  懒汉模式,类加载的时候效率高,获取单例对象时效率低,线程不安全

3、懒汉模式的非线程安全问题的解决方法:双重检测锁模式,首先使用同步代码块,同步方法,效率低下;使用DCL(Double-Check Locking)双检查锁机制-

双重判空的的意义:对于person1存在的情况,就直接返回。当person1为null并且同时存在两个线程调用getPerson1()方法时,它们都将通过第一重的person1==null的判断。

 然后由于类锁机制,这两个线程只有一个可以获得锁并进入,另一个在外排队等候,必须要其中一个进入并出来后,另一个才能进入。

 而此时如果没有了第二重的person1==null是否为null的判断,则第一个线程创建了实例,而第二个线程获得锁后还是可以继续再创建新的实例,这就没有达到单例的目的。

public class Person1 {  
    private static volatile Person1 person1 =null;  
    private Person1(){  
        }  
    public static Person1 getPerson1() {  
        if(person1==null){  
            synchronized(Person1.class){  
                if(person1==null)  
                person1=new Person1();  
            }  
        }  
        return person1;  
    }  
} 

4、静态内部类模式

public class SingleTon{
  private SingleTon(){}
 
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
}

  

25、代码块执行顺序 

案例:

class B {
    public B() {
        super();
        System.out.println("构造器B");
    }
    {
        System.out.println("普通的代码块B");
    }
    static {
        System.out.println("静态代码块B");
    }
}

public class StaticDemo extends B {
    public StaticDemo() {
        // super();
        System.out.println("构造器-StaticDemo");
    }
    {
        System.out.println("普通的代码块-StaticDemo");
    }
    static {
        System.out.println("静态代码块-StaticDemo");
    }
    //
    public static void main(String[] args) {
        StaticDemo a = new StaticDemo();
    }
}

run:

  静态代码块B
  静态代码块-StaticDemo
  普通的代码块B
  构造器B
  普通的代码块-StaticDemo
  构造器-StaticDemo

相关术语:

  1、构造代码块:(初始化代码块,非静态代码块)直接定义在类中,当调用了构造方法时,会先执行构造代码块(没有构造代码块就只执行自己)

  2、普通代码块(局部代码块):通常定义在方法内结合if switch 循环去使用。

  3、静态代码块:顾名思义static修饰的代码块。

简述类加载的初始化顺序?
  1、静态初始化块只在类加载时执行,且优先于主方法执行,且只会执行一次,同时静态初始化块只能给静态变量赋值,不能初始化普通的成员变量。
  2、可以在类加载的时候对静态的属性进行初始化
总结:
  1、所有的静态代码块先执行,从顶级父类开始依次往后加载
  2、在同一个构造器中,super() 优先于 构造代码块 优先于 构造器中的语句
  3、当构造器中的第一条语句是this()时,构造器中构造代码块不存在了

原文地址:https://www.cnblogs.com/gshao/p/10023035.html