一、什么是面向对象?
1.概念
面向对象(Object Oriented)的英文缩写是OO,它是一种设计思想。从20世纪60年代提出面向对象的概念到现在,它已经发展成为一种比较成熟的编程思想,其以人类习惯的思维方式,用对象来理解和分析问题,使开发软件的方法与过程尽可能接近人类认识的世界、解决问题的思维方法与过程。如我们经常听说的面向对象编程(Object Oriented Programming,即OOP)就是主要针对大型软件设计而提出的,它可以使软件设计更加灵活,并且能更好地进行代码复用。
2.Java中的面向对象
把现实世界中的对象抽象地体现在编程世界中,一个对象代表了某个具体的操作。一个个对象最终组成了完整的程序设计,这些对象可以是独立存在的,也可以是从别的对象继承过来的。对象之间通过相互作用传递信息,实现程序开发。
但是在面向对象设计之前,广泛采用的是面向过程,面向过程只是针对于自己来解决问题。面向过程的操作是以程序的基本功能实现为主,实现之后就完成了,也不考虑修改的可能性,面向对象,更多的是要进行子模块化的设计,每一个模块都需要单独存在,并且可以被重复利用,所以,面向对象的开发更像是一个具备标准的开发模式。
二、类与对象
类与对象是面向编程的两个重要概念。类与对象的关系即数据类型与变量之间的关系,一个类可以创建多个对象,而每个对象只能是某一个类的对象。类规定了可以用于存储什么数据,而对象用于实际存储数据,每个对象可以存储不同的数据。
类是封装对象的属性和行为的载体,反过来说,具有相同属性和行为的一类实体被称为类。而对象是类的一个具体的体现,例如,把雁群比作大雁类,那么大雁类就具备了喙、翅膀和爪等属性,觅食、飞行和睡觉等行为,而一只要从北方飞往南方的大雁则被视为大雁类的一个对象。对象关系图如2.1所示:
对象具有以下特点:
-
对象具有属性和行为。
-
对象具有变化的状态。
-
对象具有唯一性。
-
对象都是某个类别的实例。
-
一切皆为对象,真实世界中的所有事物都可以视为对象。
三、面向对象设计的特点
面向对象开发模式更有利于人们开拓思维,在具体的开发过程中便于程序的划分,方便程序员分工合作,提高开发效率。面向对象程序设计有以下优点:
-
可重用性:代码重复使用,减少代码量,提高开发效率。下面介绍的面向对象的三大核心特性(继承、封装和多态)都围绕这个核心;
-
可扩展性:指新的功能可以很容易地加入到系统中来,便于软件的修改;
-
可管理性:能够将功能与数据结合,方便管理。
该开发模式之所以使程序设计更加完善和强大,主要是因为面向对象具有的 3 个核心特性。
-
封装:保护内部的操作不被破坏;
-
继承:在原本的基础之上继续进行扩充;
-
多态:在一个指定的范围之内进行概念的转换。
1.封装
封装是面向对象编程的核心思想,其将对象的属性和行为封装起来。而将对象的属性和行为封装起来的载体就是类,类通常对客户隐藏其实现细节,这就是封装的思想。例如,用户使用计算机,只需要使用手指敲击键盘就可以实现一些功能,而无须知道计算机内部是如何工作的。
采用封装思想保证了类内部数据结构的完整性,使用该类的用户不能直接看到类中的数据结构,而只能执行类允许公开的数据,这样就避免了外部对内部数据的影响,提高了程序的可维护性。
特点:
-
良好的封装能够减少耦合。
-
类内部的结构可以自由修改。
-
可以对成员变量进行更精确的控制。
-
隐藏信息,实现细节
实现方式:
-
私有化成员变量(用private修饰成员变量)
-
为每一个成员变量提供合理的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是修饰符,可以修饰的内容:
-
可以修饰普通方法
-
可以修饰字段[ 成员变量 ]
-
可以修饰内部类[暂时不了解]
-
不可以修饰外部类
-
不可以修饰局部变量;
-
不可以修饰构造方法
作用:加了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 父类{
//...
}
注意:
-
子类将继承父类的成员变量和成员方法;
-
子类继承父类之后,需要添加自己特有的成员,体现出与基类的不同。
抽象
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
说明:
-
Animal类是动物类,每个动物都有叫的方法,但是Animal不是一个具体的动物,因此其内部的bark()方法无法具体实现。
-
Dog是狗类继承Animal类,狗是一个具体的动物,狗叫“旺旺”,其bark()可以实现。
-
Cat是猫类继承Animal类,猫是一个具体的动物,猫叫“喵喵”,其bark()可以实现。
-
因此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进行实例化。接口的使用原则如下:
-
接口必须要有子类,但此时一个子类可以使用implements关键字实现多个接口;
-
接口的子类(如果不是抽象类),那么必须要覆写接口中的全部抽象方法;
-
接口的对象可以利用子类对象的向上转型进行实例化
语法:
接口的定义格式与类的定义格式基本相同,将class关键字换成interface关键字,就定义了一个接口。
提示:
-
创建接口时,接口的命名一般以大写字母I开头
-
接口命名一般使用形容词词性的单词
-
阿里编码规范中规定,接口中的方法属性不要加任何修饰符号,保持代码的简洁性
代码示例:
/* 文件名 : 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 变量。
-
接口不是被类继承了,而是要被类实现。
-
接口支持多继承。
接口和抽象类:
从接口的概念上来讲,接口只能由抽象方法和全局常量组成,但是内部结构是不受概念限制的,正如抽象类中可以定义抽象内部类一样,在接口中也可以定义普通内部类、抽象内部类和内部接口。
主要区别:抽象类中可以包含普通字段和普通方法,这样的字段和方法可以被子类直接使用(不必重写),接口中不能包含普通方法,子类必须重写其所有抽象方法
-
一个抽象类只能继承一个抽象父类,而接口可以继承多个接口;
-
一个子类只能继承一个抽象类,却可以实现多个接口(在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();
}
}
上面的代码在多线程的环境下执行,可能会出现这种情况:
- 当前buffer是空的,这时来了一个take的请求,尚未执行到wait语句,执行了buffer.isEmpty()为true
- 同时又来了一个give请求,完整执行完了整个give方法并且发送了notify
- 此时take方法才走到wait,因为它错过了上一个notify,所以会在明明buffer不空的情况下挂起线程,take方法挂起。假如再没有人调用过give方法了,在业务上的表现就会是这个take线程永远也取不到buffer中的内容。
只要用notify,那就是为了在多线程环境下同步,notify/wait机制本身就是为了多线程的同步而存在的,那就只能配套synchronized,所以为了防止上面情况的发生,就直接强制抛异常来限制开发的代码模式了。
3.多态
将父类对象应用于子类的特征就是多态。比如创建一个螺丝类,螺丝类有两个属性:粗细和螺纹密度;然后再创建了两个类,一个是长螺丝类,一个是短螺丝类,并且它们都继承了螺丝类。这样长螺丝类和短螺丝类不仅具有相同的特征(粗细相同,且螺纹密度也相同),还具有不同的特征(一个长,一个短,长的可以用来固定大型支架,短的可以固定生活中的家具)。即,一个螺丝类衍生出不同的子类,子类继承父类特征的同时,也具备了自己的特征,并且能够实现不同的效果,这就是多态化的结构。
概念:
将子类对象装到父类的变量中保存(向上造型/向上转型),当父类变量调用方法的时候,如果子类重写了该方法,会直接执行子类重写之后的方法。(父类变量可以装任意类型的子类对象)。
-
多态是方法或对象具有多种形态,是面向对象的第三大特征。
-
多态的前提是两个对象(类)存在继承关系,多态是建立在封装和继承基础之上的
注意:
-
一个对象的编译类型与运行类型可以不一致
-
编译类型在定义对象时,就确定了,不能改变,而运行类型是可以变化的
-
编译类型看定义对象时 = 号的左边,运行类型看 = 号的右边
特点:
-
消除类型之间的耦合关系
-
可替换性
-
可扩充性
-
接口性
-
灵活性
-
简化性
注意:
-
成员变量没有多态
-
不能调用子类特有的方法,如果需要调用子类特有的方法,必须进行强制类型转换(向下造型/向下转型),向下转型需要进行子类类型判断
-
父类变量能点(调用)出哪些成员(成员变量和方法),是由当前类和其父类决定,优先从当前类 开始查找,直到找到Object了为止,如果Object中有没有,就不能调用
-
多态调用方法的优先级顺序为:(难度大)
该优先级为: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("学生要好好学习!");
}
}
运行结果:
学生好好学习