面向对象概述

面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。

面向对象的两个要素

类(class)

class是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型
类的定义规范,强制使用 阿里开发手册((四) OOP规约 )

对象(instance)

而instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同

对象的创建和使用

  1. class Person(){
  2. String name;
  3. int age;
  4. runing(){
  5. System.out.println("人在跑!");
  6. }
  7. }
  8. public class PersonTest(){
  9. Person p = new Person();
  10. p.name = "张三";
  11. p.age = 20;
  12. Person p1 = new Person();
  13. // p = p1? false
  14. System.out.println(p1.name);
  15. Person p2 = p;
  16. // 值? 张三
  17. p2.name;
  18. p1.age = 30;
  19. // p2.age ? 20
  20. System.out.println(p2.age);
  21. p2.age = 40;
  22. // 40
  23. System.out.println(p1.age);
  24. }

对象的内存解析

1、Java类和及类的成员

属性(= 成员变量=field=域、字段)

类中属性 = 成员变量

局部变量

两者比较

相同点

  1. 定义变量的格式相同, 数据类型 变量名 = 变量值
  2. 先声明,后使用
  3. 变量都有其对应的作用域

    不同点

  4. 类中声明的位置不同

    1. 属性直接定义在类内
    2. 局部变量声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量
  5. 关于权限修饰符的不同

    1. 属性:可以在声明属性时,指明其权限,使用权限修饰符
      1. 常用的权限修饰符,private,public,缺省,protected —> 封装性
    2. 局部变量,不可以使用权限修饰符。
    3. 默认初始化值
      1. 属性: 类的属性,根据类型,都是有默认初始化值
        1. 整型(byte,short, int , long): 0
        2. 浮点型(double、float):0.0
        3. 字符型(char):0 或 ‘\u0000’
        4. 布尔型(boolean) false
        5. 引用类型 (数组、类),null
      2. 局部变量
        1. 没有默认初始化值,调用前一定要初始化(空指针 异常)
        2. 形参是在调用时需要赋值
    4. 内存中加载的位置

      1. 属性,加载到堆空间(static 不在 堆空间,在静态区)
      2. 局部变量:加载在栈空间 ```java class Person(){ //属性 String name; int age;

      runing(){ System.out.println(“人在跑!”); }

      // language 形式参数 public void talk(String language){ System.out.println(language); }

  1. public void eat(){
  2. // food局部变量
  3. Stirng food = "苹果";
  4. System.out.println(food);
  5. }

}

public static void main(Stirng[] args){ Person p = new Person(); // 实参 p.talk(“汉语”); }


<a name="SDDAk"></a>
## 方法(=成员方法 = 函数=method)
<a name="WkGOL"></a>
### 方法的定义(声明)
```java

方法的声明:修饰符 方法返回类型 方法名(方法参数列表) {
            若干方法语句;
            return 方法返回值;
        }

方法权限修饰符

private 、public 、缺省、protected

方法的返回值

使用return 返回值

方法名

属于标识符,满足标识符的规则,见名知意,

形式参数

可以0个,多个

建议:

  1. 定义方法时,每个方法只做一件事
  2. 方法的方法名应见名知意,方法名一个字母为小写字母,遵循驼峰规则

方法的重载

定义:同一个类,形同方法名,不同参数列表、参数个数、参数类型

可变形参的方法

show(String … str), 可变形参的个数为 0,多个

方法参数的值传递机制(重点)

关于变量的赋值

  1. 变量为基本数据类型,此时赋值的是变量所保存的数据
  2. 变量为引用数据类型,此时赋值的是变量的地址

    方法的形参传递机制:值传递

    形参:方法定义内的参数
    实参:调用方法时实际传递的数据

    值传递机制

    参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值
    参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值,注意String类
public static void main(String[] args){

    int a = 10;
    int b = 20;

    System.out.println("n:"+n+", m:"+m);
    int temp = a;
    a = b;
    b = temp;

    System.out.println("n:"+n+", m:"+m);

    // 调用swap交换数据, 此时a、b未交换数据
    // 为什么没交换,参考 内存图
    swap(a, b);
}

// 交换数据
public void swap(int a, int b){
    int temp = a;
    a = b;
    b = temp;
}

例题:解析内存图
image.png

递归方法

定义:一个方法内调用自己。

2、面向对象的三大特征

封装

类的设计要求:高内聚、低耦合

封装性的体现,使用权限修饰属性

权限修饰符private、缺省、protected、public

修饰符 类内部 同一个包 不同包的子类 同一个工程
private yes
缺省 yes yes
protected yes yes yes
public yes yes yes yes

权限修饰符的使用位置及范围

  1. 从小到大的权限范围,private、缺省、protected、public
  2. 可以用来修饰类、类的内部结构,属性、方法、构造器、内部类
  3. 修饰类只能使用,缺省、public

    构造器(构造方法、constructor)

    作用

  4. 创建对象

  5. 赋初始值

    说明

  6. 如果没有显示的定义构造器的,系统则默认生成一个,构造器名称与类同名

  7. 定义的格式: 权限修饰符 类名(形参列表){}
  8. 类可以定义多个构造器,彼此构成重载
  9. 一旦显示定义构造器后,系统没有默认构造器

JavaBean

  1. 特征:类是公共的
  2. 有一个无参的公共的构造器
  3. 有属性,具有对应的get、set 方法

属性赋值的先后顺序

  1. 默认初始化
  2. 显示初始化
  3. 构造方法
  4. 对象赋值

执行的顺序 1 - 2- 3- 4

继承

在前面的章节中,我们已经定义了Person类:
class Person {
private String name;
private int age;

**public** String getName() {...}<br />    **public** **void** setName(String name) {...}<br />    **public** **int** getAge() {...}<br />    **public** **void** setAge(**int** age) {...}<br />}

现在,假设需要定义一个Student类,字段如下:
class Student {
private String name;
private int age;
private int score;

**public** String getName() {...}<br />    **public** **void** setName(String name) {...}<br />    **public** **int** getAge() {...}<br />    **public** **void** setAge(**int** age) {...}<br />    **public** **int** getScore() { … }<br />    **public** **void** setScore(**int** score) { … }<br />}

仔细观察,发现Student类包含了Person类已有的字段和方法,只是多出了一个score字段和相应的getScore()、setScore()方法。
能不能在Student中不要写重复的代码?
这个时候,继承就派上用场了。
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让Student从Person继承时,Student就获得了Person的所有功能,我们只需要为Student编写新增的功能。
Java使用extends关键字来实现继承:
class Person {
private String name;
private int age;

**public** String getName() {...}<br />    **public** **void** setName(String name) {...}<br />    **public** **int** getAge() {...}<br />    **public** **void** setAge(**int** age) {...}<br />}

class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;

**public** **int** getScore() { … }<br />    **public** **void** setScore(**int** score) { … }<br />}

可见,通过继承,Student只需要编写额外的功能,不再需要重复代码。
注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!
在OOP的术语中,我们把Person称为超类(super class),父类(parent class),基类(base class),把Student称为子类(subclass),扩展类(extended class)。

继承树

注意到我们在定义Person的时候,没有写extends。在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,任何类,除了Object,都会继承自某个类。下图是Person、Student的继承树:
┌───────────┐
│ Object │
└───────────┘


┌───────────┐
│ Person │
└───────────┘


┌───────────┐
│ Student │
└───────────┘

Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。
类似的,如果我们定义一个继承自Person的Teacher,它们的继承树关系如下:
┌───────────┐
│ Object │
└───────────┘


┌───────────┐
│ Person │
└───────────┘
▲ ▲
│ │
│ │
┌───────────┐ ┌───────────┐
│ Student │ │ Teacher │
└───────────┘ └───────────┘

protected

继承有个特点,就是子类无法访问父类的private字段或者private方法。例如,Student类就无法访问Person类的name和age字段:
class Person {
private String name;
private int age;
}

class Student extends Person {
public String hello() {
return “Hello, “ + name; // 编译错误:无法访问name字段
}
}

这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问:
class Person {
protected String name;
protected int age;
}

class Student extends Person {
public String hello() {
return “Hello, “ + name; // OK!
}
}

因此,protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问,后面我们还会详细讲解。

运行上面的代码,会得到一个编译错误,大意是在Student的构造方法中,无法调用Person的构造方法。
这是因为在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();,所以,Student类的构造方法实际上是这样:
class Student extends Person {
protected int score;

**public** Student(String name, **int** age, **int** score) {<br />        **super**(); _// 自动调用父类的构造方法_<br />        **this**.score = score;<br />    }<br />}

但是,Person类并没有无参数的构造方法,因此,编译失败。
解决方法是调用Person类存在的某个构造方法。例如:
class Student extends Person {
protected int score;

**public** Student(String name, **int** age, **int** score) {<br />        **super**(name, age); _// 调用父类的构造方法Person(String, int)_<br />        **this**.score = score;<br />    }<br />}

这样就可以正常编译了!
因此我们得出结论:如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。
这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

阻止继承

正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。
从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。
例如,定义一个Shape类:
public sealed class Shape permits Rect, Circle, Triangle {

}

上述Shape类就是一个sealed类,它只允许指定的3个类继承它。如果写:
public final class Rect extends Shape {…}

是没问题的,因为Rect出现在Shape的permits列表中。但是,如果定义一个Ellipse就会报错:
public final class Ellipse extends Shape {…}
// Compile error: class is not allowed to extend sealed class: Shape

原因是Ellipse并未出现在Shape的permits列表中。这种sealed类主要用于一些框架,防止继承被滥用。
sealed类在Java 15中目前是预览状态,要启用它,必须使用参数—enable-preview和—source 15。

向上转型

如果一个引用变量的类型是Student,那么它可以指向一个Student类型的实例:
Student s = new Student();

如果一个引用类型的变量是Person,那么它可以指向一个Person类型的实例:
Person p = new Person();

现在问题来了:如果Student是从Person继承下来的,那么,一个引用类型为Person的变量,能否指向Student类型的实例?
Person p = new Student(); // ???

测试一下就可以发现,这种指向是允许的!
这是因为Student继承自Person,因此,它拥有Person的全部功能。Person类型的变量,如果指向Student类型的实例,对它进行操作,是没有问题的!
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok

注意到继承树是Student > Person > Object,所以,可以把Student类型转型为Person,或者更高层次的Object。

向下转型

和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。例如:
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

如果测试上面的代码,可以发现:
Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException。
为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型:
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false

instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false。
利用instanceof,在向下转型前可以先判断:
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}

从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:
Object obj = “hello”;
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}

可以改写如下:
// instanceof variable:

Run
这种使用instanceof的写法更加简洁。

区分继承和组合

在使用继承时,我们要注意逻辑一致性。
考察下面的Book类:
class Book {
protected String name;
public String getName() {…}
public void setName(String name) {…}
}

这个Book类也有name字段,那么,我们能不能让Student继承自Book呢?
class Student extends Book {
protected int score;
}

显然,从逻辑上讲,这是不合理的,Student不应该从Book继承,而应该从Person继承。
究其原因,是因为Student是Person的一种,它们是is关系,而Student并不是Book。实际上Student和Book的关系是has关系。
具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例:
class Student extends Person {
protected Book book;
protected int score;
}

因此,继承是is关系,组合是has关系。

练习

定义PrimaryStudent,从Student继承,并新增一个grade字段:
public class Main {
public static void main(String[] args) {
Person p = new Person(“小明”, 12);
Student s = new Student(“小红”, 20, 99);
// TODO: 定义PrimaryStudent,从Student继承,新增grade字段:
Student ps = new PrimaryStudent(“小军”, 9, 100, 5);
System.out.println(ps.getScore());
}
}

class Person {
protected String name;
protected int age;

public Person(String name, int age) {<br />        this.name = name;<br />        this.age = age;<br />    }

public String getName() { return name; }<br />    public void setName(String name) { this.name = name; }

public int getAge() { return age; }<br />    public void setAge(int age) { this.age = age; }<br />}

class Student extends Person {
protected int score;

public Student(String name, int age, int score) {<br />        super(name, age);<br />        this.score = score;<br />    }

public int getScore() { return score; }<br />}

Run
image.png下载练习:继承练习(推荐使用IDE练习插件快速下载)

小结

  • 继承是面向对象编程的一种强大的代码复用方式;
  • Java只允许单继承,所有类最终的根类是Object;
  • protected允许子类访问父类的字段和方法;
  • 子类的构造方法可以通过super()调用父类的构造方法;
  • 可以安全地向上转型为更抽象的类型;
  • 可以强制向下转型,最好借助instanceof判断;
  • 子类和父类的关系是is,has关系不能用继承。
  • 继承属性new的什么,获取属性就是那个

    多态

  • 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为;

  • Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态;
  • final修饰符有多种作用:
    • final修饰的方法可以阻止被覆写;
    • final修饰的class可以阻止被继承;
    • final修饰的field必须在创建对象时初始化,随后不可修改。

image.png

多态的实现方式

方式一:重写:

这个内容已经在上一章节详细讲过,就不再阐述,详细可访问:Java 重写(Override)与重载(Overload)

方式二:接口

  1. 生活中的接口最具代表性的就是插座,例如一个三接头的插头都能接在三孔插座中,因为这个是每个国家都有各自规定的接口规则,有可能到国外就不行,那是因为国外自己定义的接口类型。
    2. java中的接口类似于生活中的接口,就是一些方法特征的集合,但没有方法的实现。具体可以看java接口这一章节的内容。

    方式三:抽象类和抽象方法

3、其他关键字

this

this 关键字主要引用当前类的对象

this 关键字调用构造器,首行,本类构造器

this(), this(1), this(“222”, 8)

supper

调用属性

调用方法

调用构造器,在第一行,表示父类构造器。在构造器中没有显示的声明,调用父类的构造器

子类对象实例化过程

  1. 从结果上,创建子类对象时,就会加载父类中所有属性和方法
  2. 从过程上,直接或间接的调用父类的构造器,直到调用Object的构造器

Dog d = new Dog(“222”)

static

可以修饰属性

属性存在于方法区

修饰方法、代码块

加载类同步加载静态方法

应用

共用属性

工具类

设计模式-单例模式

final

修饰类

修饰属性

修饰方法

abstract(is-a):模板设计

修饰类

修饰方法

继承(extends)抽象类

注意

  1. 可以有构造器,但不能实例化

    interface(can-do):契约设计

    修饰类

    实现(implements)接口

    注意

  2. JDK1.7之前只能声明常量和抽象方法

    1. public static final XXX
  3. JDK1.8及之后可以声明default的方法
    1. default void t(){}
  4. 接口中不能定义构造器,不能实例化