Java-基础篇-面向对象的三大特性

一、什么是面向对象?

1.概念

        面向对象(Object Oriented)的英文缩写是OO,它是一种设计思想。从20世纪60年代提出面向对象的概念到现在,它已经发展成为一种比较成熟的编程思想,其以人类习惯的思维方式,用对象来理解和分析问题,使开发软件的方法与过程尽可能接近人类认识的世界、解决问题的思维方法与过程。如我们经常听说的面向对象编程(Object Oriented Programming,即OOP)就是主要针对大型软件设计而提出的,它可以使软件设计更加灵活,并且能更好地进行代码复用。

2.Java中的面向对象

        把现实世界中的对象抽象地体现在编程世界中,一个对象代表了某个具体的操作。一个个对象最终组成了完整的程序设计,这些对象可以是独立存在的,也可以是从别的对象继承过来的。对象之间通过相互作用传递信息,实现程序开发。

        但是在面向对象设计之前,广泛采用的是面向过程,面向过程只是针对于自己来解决问题。面向过程的操作是以程序的基本功能实现为主,实现之后就完成了,也不考虑修改的可能性,面向对象,更多的是要进行子模块化的设计,每一个模块都需要单独存在,并且可以被重复利用,所以,面向对象的开发更像是一个具备标准的开发模式。

二、类与对象

        类与对象是面向编程的两个重要概念。类与对象的关系即数据类型与变量之间的关系,一个类可以创建多个对象,而每个对象只能是某一个类的对象。类规定了可以用于存储什么数据,而对象用于实际存储数据,每个对象可以存储不同的数据。

        类是封装对象的属性和行为的载体,反过来说,具有相同属性和行为的一类实体被称为类。而对象是类的一个具体的体现,例如,把雁群比作大雁类,那么大雁类就具备了喙、翅膀和爪等属性,觅食、飞行和睡觉等行为,而一只要从北方飞往南方的大雁则被视为大雁类的一个对象。对象关系图如2.1所示:

        对象具有以下特点:

  • 对象具有属性和行为。

  • 对象具有变化的状态。

  • 对象具有唯一性。

  • 对象都是某个类别的实例。

  • 一切皆为对象,真实世界中的所有事物都可以视为对象。

三、面向对象设计的特点

        面向对象开发模式更有利于人们开拓思维,在具体的开发过程中便于程序的划分,方便程序员分工合作,提高开发效率。面向对象程序设计有以下优点:

  1. 可重用性:代码重复使用,减少代码量,提高开发效率。下面介绍的面向对象的三大核心特性(继承、封装和多态)都围绕这个核心;

  2. 可扩展性:指新的功能可以很容易地加入到系统中来,便于软件的修改;

  3. 可管理性:能够将功能与数据结合,方便管理。

        该开发模式之所以使程序设计更加完善和强大,主要是因为面向对象具有的 3 个核心特性。

  • 封装:保护内部的操作不被破坏;

  • 继承:在原本的基础之上继续进行扩充;

  • 多态:在一个指定的范围之内进行概念的转换。

1.封装

        封装是面向对象编程的核心思想,其将对象的属性和行为封装起来。而将对象的属性和行为封装起来的载体就是类,类通常对客户隐藏其实现细节,这就是封装的思想。例如,用户使用计算机,只需要使用手指敲击键盘就可以实现一些功能,而无须知道计算机内部是如何工作的。

        采用封装思想保证了类内部数据结构的完整性,使用该类的用户不能直接看到类中的数据结构,而只能执行类允许公开的数据,这样就避免了外部对内部数据的影响,提高了程序的可维护性。

        特点:

  • 良好的封装能够减少耦合。

  • 类内部的结构可以自由修改。

  • 可以对成员变量进行更精确的控制。

  • 隐藏信息,实现细节

        实现方式:

  1. 私有化成员变量(用private修饰成员变量)

  2. 为每一个成员变量提供合理的getXxx()方法 获取成员变量的值,如果当前成员变量类型是boolean类型,将getXxx()改为 isXxx(),还有一个setXxx(...)方法 设置成员变量的值

a.this关键字

        应用场景:当本类的成员变量和局部变量重名时,使用this关键字表明成员变量

  • 哪个对象调用就是哪个对象的引用类型

  • this: 代表当前类对象的引用(地址) //就是当前类对象的地址值

  • 谁调用this,this就是谁

public class Main{
    String name;
    
    //此中出现的this.name,带表引用的对象为成员(实例、实参)变量name
    // = 号后面的name代表setName(String name)中的局部(形参)变量name
    public void setName(String name){
         this.name = name;
    }
}
this的使用方式:

        1. this.data; //访问属性

        用来访问类成员变量,用于区分类成员变量和局部变量(方法体中的变量)。

        代码示例:

public class testDemo2 {
    public static void main(String[] args) {
        Student student = new Student();
        student.name = "小明";
        student.doClass();
    }
}

public class Student {
    public String name;
    public void doClass(){
        System.out.println(name+"上课");
        this.doHomeWork();
    }
    public void doHomeWork(){
        System.out.println(name+"正在写作业");
    }   
}

        2. this.func(); //访问方法

        类中存在多个方法时,在某一个方法中通过 "this.方法" 调用另外一个方法,即让类中的一个方法,访问类中的另外一个方法或者访问类中的实例变量;

        代码示例:

        定义一个jump()方法,方法中输出内容为“正在执行jump()方法”。定义一个run()方法,方法中调用 jump()方法,利用“this.方法名”的方式调用,且调用完成之后输出内容“正在执行run()方法”

public class Dog {
 
    //定义一个jump()方法
   public void jump() {
       System.out.println("正在执行jump方法");
   }
 
   //定义一个run()方法
   public void run() {
       //通过this调用jump()方法
       this.jump();
       System.out.println("正在执行run方法");
   }
}

public class DemoApplication {
 
    public static void main(String[] args) {
 
        //利用new关键字new一个Dog类的对象
        Dog dog = new Dog();
 
        //通过dog对象调用了Dog类中的run()方法;这个方法
        //run()方法中又通过this调用了本类方法jump()
        dog.run();
 
    }

        在测试类中:

  • 通过new关键字创建一个对象(new的时候调用了默认的无参构造器);

  • 通过"对象.方法"的方式调用Dog类中的方法dog.run;

  • 最终在控制台会按顺序打印输出:"正在执行jump方法","正在执行run方法"。

        3. this(); //调用本类中其他构造方法

        作用:this() 用于访问本类的构造方法(构造器是类的一种特殊方法,方法名称和类名相同,没有返回值,括号中可以有参数,如果有参数就是有参构造方法)

  • 通过一个构造器方法1去调用另一个构造器方法2,实现代码复用

  • 构造器方法1和构造器方法2都在同一个类中

  • 语法:this(实参列表)

  • this(实参列表)的使用只能出现在第一个构造方法的第一行

        代码示例:创建一个Student学生类,利用this(“实参列表”)给实例变量name赋值

  • 通过this("实参列表")调用类中的有参构造器对name进行赋值

  • this("实参列表“)必须放在第一个构造器的第一行

  • 在测试类中。利用new关键字new一个对象出来后,打印输出name的值为甲

public class Student {
  private String name;
 
  //无参构造器
  public Student() {
 
      //通过this(“实参列表")调用类中的有参构造器对name进行赋值
      //this("有参构造")必须放在第一个构造器的第一行
      this("甲");
  }
  //有参构造器
  public Student(String name) {
      this.name = name;
  }
}

public class DemoApplication {
 
    public static void main(String[] args) {
 
        //利用new关键字new一个Student类的对象
        Student student = new Student();
        //控制台打印输出结果为甲
        System.out.println(student.name);
    }
}

b.static关键字

        对于static修饰的方法而言,可以直接使用类来调用该方法,如果在ststic修饰的方法中使用this关键字,则这个关键字就会无法指向合适的对象,所以,static修饰的方法中不能用this引用,并且Java语法规定,静态成员不能直接访问非静态成员;

        static是修饰符,可以修饰的内容:

  1. 可以修饰普通方法

  2. 可以修饰字段[ 成员变量 ]

  3. 可以修饰内部类[暂时不了解]

  4. 不可以修饰外部类

  5. 不可以修饰局部变量;

  6. 不可以修饰构造方法

        作用:加了static修饰的字段,该字段被该类所有对象共享,当一个对象修改了该字段,其他对象使用该字段,都是修改之后的值

  • 有static修饰的变量会单独放在static静态区

  • 这个变量会被所有的单位共享 变量的值等于最后去操作这个对象所赋予的值

  • 添加了static关键字的东西会被JVM优先加载

        使用方式:

        static 类级别的修饰符理解:

        1. static修饰的字段:应该通过类名.字段名的方式访问;

        2. static 修饰的方法: 应该通过类名.方法名,该字段被该类的所有对象共享。

        代码示例:

/
 *	学生类
 */
public class Student {
	/用static修饰name,会被所有用Student模板创建的对象共享*/
	static String name;
	public Student(String n) {
		name = n;//将传入的n赋值给成员变量name
	}
	public static void testStatic() {
		System.out.println("static修饰方法");
	}
	/
	 * 内部类:今天只需要了解
	 */
	static class Inner{
	}
}

/
 *	static测试类
 */
public class StudentTest {

	public static void main(String[] args) {
		//调用有参构造,创建对象并且直接赋值
		Student stu1 = new Student("隔壁老王");
		//打印stu1的名字
		System.out.println(stu1.name);//隔壁老王
		
		//调用有参构造,创建对象并且直接赋值
		Student stu2 = new Student("隔壁老孙");
		//打印stu2的名字
		System.out.println(stu2.name);//隔壁老孙
		//重新打印stu1的名字
		System.out.println(stu1.name);//隔壁老孙(居然不是隔壁老王)
	}
}
代码块执行优先级:
  • 静态代码块先执行,且只执行一次,在类加载阶段执行

  • 当有对象创建时,才会执行实例代码块,实例代码块执行完后,再执行构造方法

无继承关系时的执行顺序

        代码示例:

public class Person {
    String name;
    String gender;
    int age;
    public Person(String name,String gender,int age){
        this.name = name;
        this.gender = gender;
        this.age = age;
        System.out.println("我是构造方法");
    }
    {
        System.out.println("我是实例代码块");
    }
    static {
        System.out.println("我是静态代码块");
    }
 
    public static void main(String[] args) {
        Person p1 = new Person("xiaoHua","男",12);
        System.out.println("=====================");
        Person p2 = new Person("xiaoHong","女",15);
    }
}

------------------------------------------------
我是静态代码块
我是实例代码块
我是构造方法
=====================
我是实例代码块
我是构造方法
有继承关系时的执行顺序

        说明:

  • 父类静态代码块优先子类静态代码块执行,都是最早执行的

  • 父类实例代码块和父类构造方法紧接着执行

  • 子类的实例代码块和子类构造方法在接着执行

  • 第二次实例化对象时,父类和子类的静态代码块都不会在执行

        代码示例:

public class Person {
    String name;
    String gender;
    int age;
    public Person(String name,String gender,int age){
        this.name = name;
        this.gender = gender;
        this.age = age;
        System.out.println("person的构造方法");
    }
    {
        System.out.println("person的实例代码块");
    }
    static {
        System.out.println("person的静态代码块");
    }
}
 
 
 
public class Student extends Person{
    public Student(String name, String gender, int age) {
        super(name, gender, age);
        System.out.println("student的构造方法");
    }
    {
        System.out.println("student的实例代码块");
    }
    static {
        System.out.println("student的静态代码块");
    }
}
 
 
 
public class Text {
    public static void main(String[] args) {
        Student s1 = new Student("张三","男",35);
        System.out.println("=====================");
        Student s2 = new Student("李四","男",30);
    }
}


----------------------------------------
person的静态代码块
student的静态代码块
person的实例代码块
person的构造方法
student的实例代码块
student的构造方法
=====================
person的实例代码块
person的构造方法
student的实例代码块
student的构造方法

2.继承

        以平行四边形为例,如果把平行四边形看作四边形的延伸,那么平行四边形就复用了四边形的属性和行为,同时添加了平行四边形特有的属性和行为,如平行四边形的对边平行且相等。我们可以把平行四边形类看作是继承四边形类后产生的类,其中,将类似于平行四边形的类称为子类,将类似于四边形的类称为父类或超类。值得注意的是,在阐述平行四边形和四边形的关系时,可以说平行四边形是特殊的四边形,但不能说四边形是平行四边形。所以,继承是实现重复利用的重要手段,子类通过继承复用了父类的属性和行为的同时,又添加了子类特有的属性和行为。

        它主要解决的问题是:共性的抽取,实现代码复用。

        特点:

  • java不支持多继承,支持多重继承。但是java可以通过接口来达到多实现;

  • 子类拥有父类非 private 的属性、方法;

  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展;

  • 子类可以用自己的方式调用父类的方法;

  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。

        实现方式:

修饰符 class 父类 {
}
 
修饰符 class 子类 extends 父类{
    //...
}

        注意:

  • 子类将继承父类的成员变量和成员方法;

  • 子类继承父类之后,需要添加自己特有的成员,体现出与基类的不同。

抽象

        在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

        说明:

  1. Animal类是动物类,每个动物都有叫的方法,但是Animal不是一个具体的动物,因此其内部的bark()方法无法具体实现。

  2. Dog是狗类继承Animal类,狗是一个具体的动物,狗叫“旺旺”,其bark()可以实现。

  3. Cat是猫类继承Animal类,猫是一个具体的动物,猫叫“喵喵”,其bark()可以实现。

  4. 因此Animal可以设计为“抽象类”。

        语法:

        语法规定:抽象类不能创建对象。只能在子类中通过super使用

        注意:抽象类也是类,内部可以包含普通方法和属性和构造方法

语法:
命名一般类名AbstractXxx

修饰符 abstract class AbstractXxx{
    实例变量
	类变量
	实例方法
	类方法
	构造方法  
        
	//抽象方法:
    修饰符 abstract 返回值类型 方法名(...);
}

        代码示例:

public abstract class Animal {     //抽象类,被abstract修饰
    abstract void eat();           //抽象方法,被abstract修饰没有方法体
    abstract void sleep();
    public void run(){             //也可以增加普通方法和属性
        System.out.println("跑");
    }
}

        特性:

  • 抽象类不能直接实例化对象

  • 抽象方法不能被private修饰(抽象方法没有加访问访问修饰符,默认是public.)

  • 抽象方法不能被final和static修饰,因为抽象方法要被子类重写

  • 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类

  • 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量

  • 抽象类必须被继承,并且被继承后子类要重写父类中所有的抽象方法,如果子类也是抽象类则不用,但最终会有一个子类重写所有的抽象方法

        抽象类不能实例化对象,但是可以使用abstract声明对象,该对象可以用作其子类对象的上转型对象,那么该对象就可以调用子类重写的方法。

        代码示例:

Shape.class
public abstract class Shape {

    public abstract void draw();
    public abstract void calcArea();

    protected double area;

    public double are;

    public double getArea(){
        return this.area;
    }
}

AbstractDemo.class
public class AbstractDemo {
    public static void main(String[] args) {
        //Shape shape = new Shape();
        //编译出错,Shape是抽象类,无法实例化对象

        //正确书写格式,创建对象的同时重写抽象方法
        Shape shape1 = new Shape() {
            @Override
            public void draw() {

            }

            @Override
            public void calcArea() {

            }
        };
    }
}

接口

        接口就是公共的行为规范标准,大家在实现时,只要符合标准就可以通用。在Java中,接口可以看成:多个类的公共规范,是一种引用数据类型。

        接口声明了一组能力,但它自己并没有实现这个能力,它只是一个约定。

        由于接口里面存在抽象方法,所以接口对象不能直接使用关键字new进行实例化。接口的使用原则如下:

  1. 接口必须要有子类,但此时一个子类可以使用implements关键字实现多个接口;

  2. 接口的子类(如果不是抽象类),那么必须要覆写接口中的全部抽象方法;

  3. 接口的对象可以利用子类对象的向上转型进行实例化

        语法:

        接口的定义格式与类的定义格式基本相同,将class关键字换成interface关键字,就定义了一个接口。

        提示:

  1. 创建接口时,接口的命名一般以大写字母I开头

  2. 接口命名一般使用形容词词性的单词

  3. 阿里编码规范中规定,接口中的方法属性不要加任何修饰符号,保持代码的简洁性

        代码示例:

/* 文件名 : NameOfInterface.java */
import java.lang.*;
//引入包
 
public interface NameOfInterface{
   //任何类型 final, static 字段
   //抽象方法
}

public interface 接口名称{
    public abstract void method1();   //public abstract是固定搭配,可以省略不写
    public void method2();
    abstract void method3();
    void method4();       //推荐这种风格
}

        特性:

  • 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。

  • 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。

  • 接口中的方法都是公有的。

  • 接口中定义的变量都会被public static final隐式修饰。

  • 接口的方法默认被public abstract修饰

        接口和类:
  • 接口不能用于实例化对象。

  • 接口没有构造方法。

  • 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。

  • 接口不能包含成员变量,除了 static 和 final 变量。

  • 接口不是被类继承了,而是要被类实现。

  • 接口支持多继承。

接口和抽象类:

        从接口的概念上来讲,接口只能由抽象方法和全局常量组成,但是内部结构是不受概念限制的,正如抽象类中可以定义抽象内部类一样,在接口中也可以定义普通内部类、抽象内部类和内部接口。

        主要区别:抽象类中可以包含普通字段和普通方法,这样的字段和方法可以被子类直接使用(不必重写),接口中不能包含普通方法,子类必须重写其所有抽象方法

  1. 一个抽象类只能继承一个抽象父类,而接口可以继承多个接口;

  2. 一个子类只能继承一个抽象类,却可以实现多个接口(在Java中,接口的主要功能是解决单继承局限问题)

        注意:

  • 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。

  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。

  • 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。

  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。:JDK 1.8 以后,接口里可以有静态方法和方法体了。

:JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为"默认方法",默认方法使用 default 关键字修饰。更多内容可参考 Java 8 默认方法

:JDK 1.9 以后,允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。更多内容可参考 Java 9 私有接口方法

单继承和多实现:

        在Java中类与类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承,接口间的继承相当于把多个接口合并起来

        接口之间可以继承,达到复用的效果,使用 extends 关键字。

 // 文件名: Sports.java
 public interface Sports {
    public void setHomeTeam(String name);
    public void setVisitingTeam(String name);
 }
  
 // 文件名: Football.java
 public interface Football extends Sports {
    public void homeTeamScored(int points);
    public void visitingTeamScored(int points);
    public void endOfQuarter(int quarter);
 }
  
 // 文件名: Hockey.java
 public interface Hockey extends Sports {
    public void homeGoalScored();
    public void visitingGoalScored();
    public void endOfPeriod(int period);
    public void overtimePeriod(int ot);
 }

        Hockey接口自己声明了四个方法,从Sports接口继承了两个方法,这样,实现Hockey接口的类需要实现六个方法。

        相似的,实现Football接口的类需要实现五个方法,其中两个来自于Sports接口

        多继承:

        在Java中,类的多继承是不合法,但接口允许多继承。

        在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口。 如下所示:

public interface Hockey extends Sports, Event{

}

        以上的程序片段是合法定义的子接口,与类不同的是,接口允许多继承,而 Sports及 Event 可以定义或是继承相同的方法。

类的继承与接口类的继承与接口可以共存:

        类可以在继承基类的情况下,同时实现一个或多个接口,语法如下所示(用于演示,Base这个类并没有定义):

 public class Child extends Base implements IChild(){
     //主体代码
 }

        关键字extends要放在implements之前。

Object

        Object类是所有类的父类,所有类都继承Object类,即所有类的对象都可以使用Object的引用进行接收。所有类的显式或隐式的继承了Object类。

super关键字

        super();调用父类的构造方法。

  • 使用super();时,调用父类的无参构造方法

  • 使用super(参数列表);调用父类的有参构造方法

        如果子类中存在与父类同名的方法成员,则通过关键字super在子类方法中访问父类方法成员

        注意事项:只能在非静态方法中使用。

        子类的构造方法里必须调用父类的构造方法:

  • 若父类里没有写任何的构造方法,且子类没用super调用,或者使用super()。则调用系统默认生成的空参数的构造方法。

  • 若父类写了有参数的构造方法

    • 子类使用了super(form parameter)不会产生错误

    • 子类没用super调用,或者使用super()则会产生错误。此时必须在父类添加无参数的构造方法,或正确调用。

    • 调用父类中被重写的方法、变量

        代码示例:

class FatherClass {
    public int value;
    public void f(){
        value = 100;
        System.out.println
        ("FatherClass.value="+value);
    }
}

class ChildClass extends FatherClass {
    public int value;
    public void f() {
        super.f();//调用父类的法f()方法
        value = 200;//修改子类对象的value值
        System.out.println
             ("ChildClass.value="+value);
        System.out.println(value);
        System.out.println(super.value);//父类中的成员变量值已经从0变为100
    }
}

public class TestInherit {
	public static void main(String[] args) {
		ChildClass cc = new ChildClass();
		cc.f();
	}
}

------------------------------
FatherClass.value=100
ChildClass.value=200
200
100
final关键字

        final 可以用来修饰变量(包括类属性、对象属性、局部变量和形参)、方法(包括类方法和对象方法)和类。

        关键字,修饰符,表示最终的。就是一旦修饰一个成员,该成员就不能再被修改了。

        使用 final 关键字声明类,就是把类定义定义为最终类,不能被继承,或者用于修饰方法,该方法不能被子类重写:

        可以修饰:

  • 外部类:太监类,不能被继承

  • 实例变量:必须在声明的时候赋值或者在构造方法中赋值

  • 类变量: 必须在声明的时候赋值

  • 实例方法:不能被子类重写

  • 类方法:不能被子类重写

  • 内部类:不能被继承

  • 局部变量:

        不能修饰:

  • 构造方法

注: final 定义的类,其中的属性、方法不是 final 的。

hashCode

        获取对象的哈希码值,为16进制。

equals

         Object类中的equals方法,用来比较两个引用的虚地址。当且仅当两个引用在物理上是同一个对象时,返回值为true,否则将返回false。不过,一般子类都会重写该方法。

toString

        toString方法可以将任何一个对象转换成字符串返回,返回值的生成算法为:getClass().getName() + '@' + Integer.toHexString(hashCode())。不过,一般子类都会重写该方法。

为什么重写equals方法必须重写hashCode方法

        因为如果只重写了equals方法而没有重写hashCode方法,则两个不同的实例a和b虽然equals结果相等(业务逻辑上,比如:两个对象的值全部相等),但却会有不同的hashcode,这样hashmap里面会同时存在a和b,而实际上我们需要hashmap里面只能保存其中一个,因为从业务逻辑方向看它们是相等的。

        equals方法和hashCode方法如果不同时按你自己逻辑重写的话,HashMap就会出问题。比如你只重写了equals方法而没有重写hashCode方法,那么HashMap在第一步寻找链表的时候会出错,有同样值的两个对象a和b并不会指向同一个链表或桶,因为你没有提供自己的hashCode方法,那么就会使用Object的hashCode方法,该方法是根据内存地址来比较两个对象是否一致,由于a和b有不同的内存地址,所以会指向不同的链表,这样HashMap会认为b不存在,虽然我们期望a和b是同一个对象;反之如果只重写了hashCode方法而没有覆盖equals方法,那么虽然第一步操作会使a和b找到同一个链表,但是由于equals没有覆盖,那么在遍历链表的元素时,a.equals(b)也不为true(事实上Object的equals方法也是比较内存地址),从而HashMap认为不存在b对象,这同样也是不正确的。

为什么wait、notify必须强制要求放在synchronized中?

        在某些情况下,线程需要交替执行。比如一个线程向一个存储单元执行存放数据,而另一个操作执行取值操作,线程间同步完成这个存取任务,需要将这些线程同步。要解决线程交替执行但是又要保证共享资源安全,这需要使用到wait()、notify()方法。

        synchronized同步块包裹着Object.wait()方法,如果不通过同步块包住的话JVM会抛出IllegalMonitorStateException异常。

        假设Object.wait()/notify不需要同步,我们来实现一个BlockingQueue的代码。代码示例:

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();
 
    public void give(String data) {
        buffer.add(data);
        notify();                   // 往队列里添加的时候notify,因为可能有人在等着take
    }
 
    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // 用while,防止spurious wakeups(虚假唤醒)
            wait(); // 当buffer是空的时候就等着别人give
        return buffer.remove();
    }
}

         上面的代码在多线程的环境下执行,可能会出现这种情况:

  1. 当前buffer是空的,这时来了一个take的请求,尚未执行到wait语句,执行了buffer.isEmpty()为true
  2. 同时又来了一个give请求,完整执行完了整个give方法并且发送了notify
  3. 此时take方法才走到wait,因为它错过了上一个notify,所以会在明明buffer不空的情况下挂起线程,take方法挂起。假如再没有人调用过give方法了,在业务上的表现就会是这个take线程永远也取不到buffer中的内容。

        只要用notify,那就是为了在多线程环境下同步,notify/wait机制本身就是为了多线程的同步而存在的,那就只能配套synchronized,所以为了防止上面情况的发生,就直接强制抛异常来限制开发的代码模式了。

3.多态

        将父类对象应用于子类的特征就是多态。比如创建一个螺丝类,螺丝类有两个属性:粗细和螺纹密度;然后再创建了两个类,一个是长螺丝类,一个是短螺丝类,并且它们都继承了螺丝类。这样长螺丝类和短螺丝类不仅具有相同的特征(粗细相同,且螺纹密度也相同),还具有不同的特征(一个长,一个短,长的可以用来固定大型支架,短的可以固定生活中的家具)。即,一个螺丝类衍生出不同的子类,子类继承父类特征的同时,也具备了自己的特征,并且能够实现不同的效果,这就是多态化的结构。

        概念:

        将子类对象装到父类的变量中保存(向上造型/向上转型),当父类变量调用方法的时候,如果子类重写了该方法,会直接执行子类重写之后的方法。(父类变量可以装任意类型的子类对象)。

  • 多态是方法或对象具有多种形态,是面向对象的第三大特征

  • 多态的前提是两个对象(类)存在继承关系,多态是建立在封装和继承基础之上的

        注意

  • 一个对象的编译类型与运行类型可以不一致

  • 编译类型在定义对象时,就确定了,不能改变,而运行类型是可以变化的

  • 编译类型看定义对象时 = 号的左边运行类型看 = 号的右边

        特点:

  • 消除类型之间的耦合关系

  • 可替换性

  • 可扩充性

  • 接口性

  • 灵活性

  • 简化性

        注意:

  1. 成员变量没有多态

  2. 不能调用子类特有的方法,如果需要调用子类特有的方法,必须进行强制类型转换(向下造型/向下转型),向下转型需要进行子类类型判断

  3. 父类变量能点(调用)出哪些成员(成员变量和方法),是由当前类和其父类决定,优先从当前类 开始查找,直到找到Object了为止,如果Object中有没有,就不能调用

  4. 多态调用方法的优先级顺序为:(难度大)

    该优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

转型

向上转型

        本质:父类的引用指向子类的对象

        特点

  • 编译类型看左边,运行类型看右边;

  • 可以调用父类的所有成员(需遵守访问权限);

  • 不能调用子类的特有成员;

  • 运行效果看子类的具体实现。

        语法

父类类型 引用名 = new 子类类型();//右侧创建一个子类对象,把它当作父类看待使用
父类变量.方法();//子类若重写,则会执行子类重写后的方法
向下转型

        本质:一个已经向上转型的子类对象,将父类引用转为子类引用

        特点

  • 只能强制转换父类的引用,不能强制转换父类的对象

  • 要求父类的引用必须指向的是当前目标类型的对象

  • 当向下转型后,可以调用子类类型中所有的成员

        语法

子类类型 引用名 = (子类类型) 父类引用;
//用强制类型转换的格式,将父类引用类型转为子类引用类型

        代码示例:
        现在需要调用子类Person中特有的方法或者调用子类Pig中特有的方法,不能调用怎么办?
        这个时候就需要强制转换(向下造型/向下转型):

  • 强制转换(向下造型/向下转型)语法:    
  • 数据类型 变量 = (数据类型)值/变量;

        多态案例:

        父类:

public class Vip {
	/**
	 * 会员特权
	 */
	public void privilege() {
		System.out.println("我有特权...");
	}
}

        子类:

public class Vip1 extends Vip{
	/**
	 * 会员特权
	 */
	public void privilege() {
		System.out.println("我是5000元俱乐部会员");
	}
	public void low() {
		System.out.println("我很low");
	}
}

public class Vip2 extends Vip{
	/**
	 * 会员特权
	 */
	public void privilege() {
		System.out.println("我是500000元俱乐部会员");
	}
	public void normal() {
		System.out.println("我很一般");
	}
}


public class Vip3 extends Vip{
	/**
	 * 会员特权
	 */
	public void privilege() {
		System.out.println("我是500000000000元俱乐部会员");
	}
	public void great() {
		System.out.println("我很NB....");
	}
}

        测试类:

public class VipTest {

	public static void main(String[] args) {
		/*
		 * 模拟会员登陆的时候特权展示
		 * 
		 */
		Vip vip = new Vip2();//屏蔽子类差异性
		
		//调用特权
		vip.privilege();//调用特权方法,如果子类重写了,会执行对应子类重写后的方法
		
		//判断当前VIP中装的子类是哪一个,展示对应的特权
		if (vip instanceof Vip1) {//类型判断
			//强制转换
			Vip1 vip1 = (Vip1)vip;
			//调用特有方法
			vip1.low();
		}else if (vip instanceof Vip2) {//类型判断
			//强制转换
			Vip2 vip2 = (Vip2)vip;
			//调用特有方法
			vip2.normal();
		}else if (vip instanceof Vip3) {//类型判断
			//强制转换
			Vip3 vip3 = (Vip3)vip;
			//调用特有方法
			vip3.great();
		}
	}

}
类型转换异常

        在向下造型前,必须进行类型判断,需要判断当前父类变量中装的是哪一个子类类型的对象,如果不进行类型判断再强转,就有可能发生ClassCastException类造型异常。

        代码示例:

public class AnimalTest {

	public static void main(String[] args) {
		Animal animal = new Person();//多态的方式(向上造型/向上转型)
		Animal animal2 = new Pig();//多态的方式(向上造型/向上转型)
		animal = new Pig();//多态的方式(向上造型/向上转型)
		
		//调用方法
		animal.eat();
		animal2.eat();
		System.out.println(animal.age);//1     成员变量没有多态 
		
//如果这里需要调用Pig类中特有方法,就需要将animal强转为Pig类型
		//类型判断的方式1
		if (animal instanceof Pig) {//判断animal中是不是装的Pig对象,如果是才强转
			Pig pig = (Pig)animal;
			//调用Pig特有方法
			pig.gongBaiCai();
		} else if (animal instanceof Person) {
			//如果这里需要调用Person类中特有方法,就需要将animal强转为Person类型
			Person person = (Person)animal;
			//调用Person特有方法
			person.coding();
		}
		
		//类型判断的方式2
		if (animal.getClass() == Pig.class) {//判断animal中是不是装的Pig对象,如果是才强转
			Pig pig = (Pig)animal;
			//调用Pig特有方法
			pig.gongBaiCai();
		} else if (animal.getClass() == Person.class) {
			//如果这里需要调用Person类中特有方法,就需要将animal强转为Person类型
			Person person = (Person)animal;
			//调用Person特有方法
			person.coding();
		}
	}
}  

        为了避免上述类型转换异常的问题,我们引出 instanceof 比较操作符,用于判断对象的类型是否为XX类型或XX类型的子类型

  • 格式:对象 instanceof 类名称

  • 解释:这将会得到一个boolean值结果,也就是判断前面的对象能不能当作后面类型的实例

类型判断方式1:
    if(父类变量 instanceof 子类类型1){
        //强制类型转换
        子类类型1	子类变量 = (子类类型1)父类变量;
        //现在就可以调用子类特有方法
        子类变量.子类特有方法(...);
    }else if(父类变量 instanceof 子类类型2){
        //强制类型转换
        子类类型2	子类变量 = (子类类型2)父类变量;
        //现在就可以调用子类特有方法
        子类变量.子类特有方法(...);
    }...
           
---------------------------------------------------------
 类型判断方式2:
  	if(父类变量.getClass() == 子类类型1.class){
  		//强制类型转换
		子类类型1	子类变量 = (子类类型1)父类变量;
		//现在就可以调用子类特有方法
		子类变量.子类特有方法(...);
	}else if(父类变量.getClass() == 子类类型2.class){
		//强制类型转换
		子类类型2	子类变量 = (子类类型2)父类变量;
		//现在就可以调用子类特有方法
		子类变量.子类特有方法(...);
	}...
动态绑定
  • 当调用对象方法的时候,该方法会和该对象的运行类型绑定

  • 当调用对象属性时,没有动态绑定机制,即哪里声明,哪里使用。

        代码示例:

//演示动态绑定
public class DynamicBinding {
	public static void main(String[] args) {
		//向上转型(自动类型转换)
		//程序在编译阶段只知道 p1 是 Person 类型
		//程序在运行的时候才知道堆中实际的对象是 Student 类型	
		Person p1 = new Student();  
		
		//程序在编译时 p1 被编译器看作 Person 类型
		//因此编译阶段只能调用 Person 类型中定义的方法
		//在编译阶段,p1 引用绑定的是 Person 类型中定义的 mission 方法(静态绑定)
		//程序在运行的时候,堆中的对象实际是一个 Student 类型,而 Student 类已经重写了 mission 方法
		//因此程序在运行阶段对象中绑定的方法是 Student 类中的 mission 方法(动态绑定)
		p1.mission();
	}
}

//父类
class Person {
	public void mission() {	
		System.out.println("人要好好活着!");
	}
}

//子类
class Student extends Person {
	@Override
	public void mission() {	
		System.out.println("学生要好好学习!");
	}
}


运行结果:
    学生好好学习

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值