Java面向对象

面向对象

1.面向对象的概念

面向对象和面向过程的区别

面向过程:

  • 将问题分解成步骤,然后按照步骤实现函数,执行时一次调用函数。
  • 数据和对数据的操作是分离的。

面向对象:

  • 将问题分解成对象,描述事物在解决问题的步骤中的行为。、
  • 对象与属性和行为是关联的。

面向过程的优点

  • 性能比面向对象高,不需要面向对象的实例化。
  • 缺点是不容易维护、复用、扩展。

面向对象的优点

  • 具有封装、继承、多态的特征,因而容易维护、复用、拓展。可以设计出低耦合的系统。
  • 缺点是由于需要实例化对象,因此性能比面向过程低。

对象和类

对象是现实生活中可以明确标识的实体。

  • 对象具有自己独有的状态和行为。

  • 对象的状态由数据域的集合构成。

  • 对象的行为由方法的集合构成。

类是定义同一类型对象的结构。

  • 类是具有相同特征的对象的抽象。

  • 类是一个模板,用来定义对象的数据域和方法。

  • 可以从一个类创建多个对象,创建对象成为“实例化”。


2.构造方法

构造方法是一种特殊的方法,调用构造方法可以创建新对象。

构造方法可以执行任何操作,实际应用中,构造方法一般用于初始化操作,例如初始化对象的数据域。

定义和调用构造方法

  • 构造方法的名称必须和构造方法所在类的名称相同。
  • 构造方法可以被重载,即允许在同一个类中定义多个参数列表不同的构造方法。
  • 使用 new 操作符调用构造方法,通过调用构造方法创建对象。

默认构造方法

类可以不显性声明构造方法。此时类中隐性声明了一个方法体为空的没有参数的构造方法,称为默认构造方法

  • 只有当类中没有显性声明任何构造方法时,才会有默认构造方法。

构造方法和普通方法的区别

  • 构造方法的名称必须是与所在的类的名称相同。
  • 构造方法没有返回类型,包括没有void
  • 构造方法通过new操作符调用,通过调用构造方法创建对象。

3.静态与实例

Java 的类成员(成员变量、方法等)可以是静态的或实例的。

使用关键字 static 修饰的类成员是静态的类成员。

不使用关键字 static 修饰的类成员则是实例的类成员。

静态和实例的区别

外部调用

  • 从外部调用静态的类成员时,可以通过类名调用,也可以通过对象名调用。

  • 从外部调用实例的类成员,则只能通过对象名调用。

    建议通过类名调用静态的类成员,因为通过类名调用静态的类成员是不需要创建对象的,而且可以提高代码的可读性。

内部访问

  • 静态方法只能访问静态的类成员,不能访问实例的类成员。

  • 实例方法既可以访问实例的类成员,也可以访问静态的类成员。

    为什么静态方法不能访问实例的类成员呢?

    ​ 因为实例的类成员是依赖于具体对象(实例)的,而静态方法不依赖于任何实例,因此不存在静态方法直接或间接地访问实例或实例的类成员的情况。

判断使用静态或实例

取决于类成员是否依赖于具体实例。

  • 如果一个类成员依赖于具体实例,则该类成员应该被定义成实例的类成员,否则就应该被定义成静态的类成员。
  • 对于数学类 Math,所有的类成员都不依赖于具体的实例,因此都被定义成静态的类成员。

静态变量在方法区,实例变量在堆内存


4.初始化块

代码初始化块属于类成员,在加载类时或创建对象时,会隐式调用代码初始块。

初始化块的好处时可以减少多个构造器内的重复代码。

初始化块的分类

初始化块可以被分为静态初始化块和非静态初始化块。

前者在加载类时被隐式调用,后者在加载对象时被隐式调用。

单个类的初始化块的执行顺序

如果有初始化块,则初始化块会在其他代码之前被执行。

  • 静态代码块会在静态方法之前被执行。
  • 非静态初始化块会在构造器和实例方法之前被执行。

由于,静态代码块在加载类时被调用,因此,静态初始化块总是会被最先执行。且只会执行一次。

由于,非静态初始化块在创建对象时进行调用,因此,每次创建对象时都会执行非静态初始化块以及执行构造器。非静态初始化块的执行在静态初始化块之后、构造器的执行之前。

存在继承关系的初始化块执行顺序

如果存在继承关系,则在对子类进行类的加载和创建对象时,也会对父类进行类的加载和创建对象。

执行顺序依然是:静态初始化块、非静态初始化块、构造器

对于两个类的情况,即一个父类一个子类,执行顺序如下:

  1. 执行父类的静态初始化块
  2. 执行子类的静态初始化块
  3. 执行父类的非静态初始化块
  4. 执行父类的构造器
  5. 执行子类的非静态初始化块
  6. 执行子类的构造器

对于更一般的情况:

  1. 按照继承关系,从父类到子类的顺序,依次执行每个类的静态初始化块。
  2. 按照继承关系,从父类到子类的顺序,依次执行每个类的非静态代码块和构造器,然后执行子类的非静态初始化块和构造器,直到所有类执行完毕。

5.关键字this

关键字this代表当前对象的引用。当前对象指的是调用类中的属性或方法的对象。

关键字 this 用于引用隐藏变量

在方法和构造方法中,可能将属性名用作参数名,在这种情况下,需要引用隐藏的属性名才能给属性设置新值。例如,当属性名和参数名都是 var 时,需要通过 this.var = var 对属性进行赋值。

当方法内部有局部变量和属性名相同时,同样需要通过关键字 this 引用对象的属性。

如果方法内部不存在和属性名相同的局部变量,则在使用属性时,属性前面的 this 可以省略。

关键字 this 用于调用其他构造方法

在构造方法中,可以通过关键字 this 调用其他构造方法,具体用法是 this(参数列表)。

Java 要求,在构造方法中如果使用关键字 this 调用其他构造方法,则 this(参数列表) 语句必须出现在其他语句之前。

关键字 this 不能在静态代码块中使用

由于关键字 this 代表的是对象的引用,因此依赖于具体对象,而静态方法和静态初始化块不依赖于类的具体对象,因此静态方法和静态初始化块中不能使用关键字 this。


6.可见性修饰符和数据域封装

Java 的可见性修饰符用于控制对类成员的访问。可见性修饰符包括 publicprivateprotected 和默认修饰符.

不同的可见性修饰符的含义

可见性修饰符 public 表示类成员可以在任何类中访问。

可见性修饰符 private 表示类成员只能从自身所在的类中访问。

如果不加任何可见性修饰符,则称为默认修饰符,表示类成员可以在同一个包里的任何类中访问,此时也称为包私有或包内访问。

可见性修饰符 类内访问 包内访问 从不同包访问
public 可以 可以 可以
默认 可以 可以 不可以
private 可以 不可以 不可以

数据与封装

可见性修饰符可以用于控制对类成员的访问,也可以用于对数据域进行封装。

数据与封装的含义是,对数据域使用 private 修饰符,将数据域声明为私有域。

如果不使用数据域封装,则数据域的值可以从类的外部直接修改,导致数据被篡改以及类难以维护。使用数据域封装的目的是为了避免直接修改数据域的值。

在定义私有数据域的类之外,不能通过直接引用的方式访问该私有数据域,但是仍然可能需要读取和修改数据域的值。

为了能够读取私有数据域的值,可以编写 get 方法(称为读取器或访问器)返回数据域的值。为了能够修改私有数据域的值,可以编写 set 方法(称为设置器或修改器)将数据域的值设置为新值。


7.字符串

字符串是常用的数据类型,在Java中,常见的字符串类型包括StringStringBufferStringBuilder

String

String使用数组存储字符串的内容,数组使用关键字final修饰,因此数组内容不可变,使用String定义的字符串的值也是不可变的。

注:由于 String 类型的值不可变,因此每次对 String 的修改操作都会创建新的 String 对象,导致效率低下且占用大量内存空间

StringBuffer StringBuilder

StringBuffer StringBuilder 都是 AbstractStringBuilder 的子类,

同样使用数组存储字符串的内容,由于数组没有使用关键词 final 修饰,因此数组内容可变,

StringBuffer StringBuilder 都是可变类型,可以对字符串的内容进行修改,且不会因为修改而创建新的对象

StringBuffer StringBuilder 有哪些区别呢?

  • 从源码可以看到,StringBuffer 对定义的方法或者调用的方法使用了关键词 synchronized 修饰,而 StringBuilder 的方法没有使用关键词 synchronized 修饰。
  • 由于 StringBuffer 对方法加了同步锁,因此其效率略低于 StringBuilder,但是在多线程的环境下,StringBuilder 不能保证线程安全,因此 StringBuffer 是更优的选择。

总结

  1. String 是不可变类型。

    ​ 每次对 String 的修改操作都会创建新的 String 对象,导致效率低下且占用大量内存空间,因此 String 适用于字符串常量的情形,不适合需要对字符串进行大量修改的情形。

  2. StringBuffer 是可变类型,适用于多线程环境。

    ​ 可以修改字符串的内容且不会创建新的对象,且 StringBuffer 是线程安全的,适用于多线程环境。

  3. StringBuilder 是可变类型,不适合多线程环境。

    ​ 与 StringBuffer 相似,在单线程环境下 StringBuilder 的效率略高于 StringBuffer,但是在多线程环境下 StringBuilder 不保证线程安全,因此 StringBuilder 不适合多线程环境。


8.继承

在面向对象程序设计中,可以从已有的类(父类)派生出新的(子类),称为继承。

父类和子类

如果已有的类 C1 派生出一个新类 C2,则称 C1 为 C2 的父类,C2 为 C1 的子类。子类从父类中继承可访问的类成员,也可以添加新的类成员。

子类通常包含比父类更多的类成员。

继承用来为 is-a 关系建模,子类和父类之间必须存在 is-a 关系

如果一个类在定义时没有指定继承,它的父类默认是 Object

关键字super

关键字 super 指向当前类的的父类。

关键字 super 可以用于两种途径:

  • 一是调用父类的构造方法
  • 二是调用父类的方法

调用父类的构造方法,使用super()super(参数),该语句必须是子类构造方法的第一个语句,且这是调用父类构造方法的唯一方式。

调用父类的方法,使用super.方法名(参数)

构造方法链

如果构造方法没有显式地调用同一个类中其他的构造方法或父类的构造方法,将隐性地调用父类的无参数构造方法,即编译器会把 super() 作为构造方法的第一个语句。

构造一个类的实例时,将会沿着继承链调用所有父类的构造方法,父类的构造方法在子类的构造方法之前调用,称为构造方法链

方法的重写

子类从父类中继承方法。

如果子类修改了父类中定义的方法,则称为方法重写。

方法重写要求子类的方法和父类的方法的签名相同。

如果方法的返回值类型是基本数据类型或者 void,则要求子类的方法的返回值类型和父类的方法的返回值类型相同。如果方法的返回值类型是引用类型,则要求返回值类型相同或者子类的方法的返回值类型是父类的方法的返回值类型的子类。

实例方法只有当可访问时才能被重写。由于私有方法不能在定义该方法的类外访问,因此私有方法不能被重写。

静态方法可以被继承,但是不能被重写。

重写和重载的区别

重载指在同一个类中定义多个方法,这些方法有相同的名称,但是方法签名不同。

重写指在子类中定义一个方法,该方法与父类中的方法的签名相同,返回值类型相同或者子类的方法的返回值类型是父类的方法的返回值类型的子类。

重载 重写
场合 同一个类 父类和子类
方法名称 相同 相同
方法签名 不同 相同
返回值类型 可以不同 相同或者子类返回值类型是父类返回值类型的子类

可见性修饰符 protected

可见性修饰符 protected 的可见性在 public 和默认之间,表示类成员可以在同一个包里的任何类中访问,也可以在该类的子类中访问。

子类可以覆盖父类的 protected 方法,并把该方法的可见性改成 public。但是子类不能降低父类方法的可见性,即不能把父类的 public 方法的可见性改成 protected。

关键字 final

关键字 final 可以用于声明常量,表示常量不会改变。

关键字 final 也可以用于修饰类和方法。

使用 final 修饰的类是最终类,不能被继承。

使用 final 修饰的方法不能被子类重写。


9.Object类的部分方法

toString

方法定义:

public String toString();

该方法返回一个代表该对象的字符串。

该方法的默认实现返回的字符串在绝大多数情况下是没有信息量的,因此通常都需要在子类中重写该方法。

equals

方法定义:

public boolean equals(Object obj);

该方法检验两个对象是否相等。

该方法的默认实现使用 == 运算符检验两个对象是否相等,通常都需要在子类中重写该方法。

hashCode

方法定义:

public native int hashCode();

该方法返回对象的散列码。

关键字 native 表示实现方法的编程语言不是 Java。

散列码是一个整数,用于在散列集合中存储并能快速查找对象。

根据散列约定,如果两个对象相同,他们的散列码一定相同。因此如果在子类中重写了 equals 方法,必须在该子类中重写 hashCode 方法,以保证两个相等的对象对应的散列码是相同的。

两个相等的对象一定具有相同的散列码,两个不同的对象也可能具有相同的散列码。实现 hashCode 方法时,应避免过多地出现两个不同的对象也可能具有相同的散列码的情况。

finalize

方法定义:

protected void finalize() throws Throwable

该方法用于垃圾回收。

如果一个对象不再能被访问,就变成了垃圾,finalize 方法会被该对象的垃圾回收程序调用。

该方法的默认实现不做任何事,如果必要,子类应该重写该方法。

该方法可能抛出 Throwable 异常。

clone

方法定义:

protected native Object clone() throws CloneNotSupportedException

该方法用于复制一个对象,创建一个有单独内存空间的新对象。

不是所有的对象都可以复制,只有当一个类实现了 java.lang.Cloneable 接口时,这个类的对象才能被复制。

该方法可能抛出CloneNotSupportedException异常。

getClass

方法定义:

public final native Class<?> getClass()

该方法返回对象的元对象。

元对象是一个包含类信息的对象,包括类名、构造方法和方法等。

一个类只有一个元对象。每个对象都有一个元对象,同一个类的多个对象对应的元对象相同。


10.抽象类和接口

抽象类指抽象而没有具体实例的类。

接口是一种与类相似的结构,在很多方面与抽象类相近。

抽象类

抽象类使用关键字abstract修饰。

抽象类和常规类一样具有数据域,方法和构造方法,但是不能用new操作符创建实例。

抽象类可以包含抽象方法。抽象方法使用关键字 abstract 修饰,只有方法签名而没有实现,其实现由子类提供。抽象方法都是非静态的。包含抽象方法的类必须声明为抽象类。

非抽象类不能包含抽象方法。如果一个抽象父类的子类不能实现所有的抽象方法,则该子类也必须声明为抽象类。

包含抽象方法的类必须声明为抽象类,但是抽象类可以不包含抽象方法。

接口

接口使用关键字 interface 定义。接口只包含可见性为 public 的常量和抽象方法,不包含变量和具体方法。

和抽象类一样,接口不能用 new 操作符创建实例。

新版本的 JDK 关于接口的规则有以下变化。

  • 从 Java 8 开始,接口方法可以由默认实现。
  • 从 Java 9 开始,接口内允许定义私有方法。

一个类只能继承一个父类,但对接口允许多重继承。一个接口可以继承多个接口,这样的接口称为子接口。

抽象类和接口的区别

  • 抽象类的变量没有限制,接口只包含常量,即接口的所有变量必须是 public static final
  • 抽象类包含构造方法,子类通过构造方法链调用构造方法,接口不包含构造方法。
  • 抽象类的方法没有限制,接口的方法必须是 public abstract 的实例方法。新版本JDK对该规则有修改
  • 一个类只能继承一个父类,但是可以实现多个接口。一个接口可以继承多个接口。

自定义比较方法Comparable&Comparator

有两个接口可以实现对象之间的排序和比较大小。

Comparable 接口是排序接口。

如果一个类实现了 Comparable 接口,则该类的对象可以排序。

Comparable 接口包含一个抽象方法 compareTo,实现 Comparable 接口的类需要实现该方法,定义排序的依据。

Comparator 接口是比较器接口。

如果一个类本身不支持排序(即没有实现 Comparable 接口),但是又需要对该类的对象排序,则可以通过实现 Comparator 接口的方式建立比较器。

Comparator 接口包含两个抽象方法 compare 和 equals,其中 compare 方法是必须在实现类中实现的,而 equals 方法在任何类中默认已经实现。

如果需要对一个数组或列表中的多个对象进行排序,则可以将对象的类定义成实现 Comparable 接口,也可以在排序时定义 Comparator 比较器。


11.基本数据类型和包装类

Java 有 8 种基本数据类型,基本数据类型不属于对象。但是在很多时候需要把对象作为参数,因此需要把基本数据类型包装成对象,对应的类称为包装类。

基本数据类型和包装类的对应关系

每种基本数据类型都有对应的包装类,如以下表格所示。

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

包装类的名称和对应的基本数据类型相同,包装类的名称的首字母大写,IntegerCharacter 例外。

包装类的构造方法

  • 可以通过包装类的构造方法创建包装对象。调用构造方法时,构造方法的参数值可以是基本数据类型的值,也可以是表示值的字符串。

  • 包装类的构造方法都是有参数的,没有无参数构造方法。

  • 包装类的实例都是不可变的,一旦创建了包装对象,其内部的值就不能再改变。

自动装箱和自动拆箱

从 JDK 1.5 开始,基本数据类型和包装类之间可以进行自动转换。

将基本数据类型的值转换为包装对象,称为装箱。

将包装对象转换为基本数据类型的值,称为拆箱。

关于java包装类缓存和常量池

Byte,Short,Integer,Long,Character,Boolean这六类包装类默认创建对应的缓存,如下表所示

类型 缓存范围
Byte,Short,Integer,Long [-128, 127]
Character [0, 127]
Boolean [false, true]

其中,Integer类型可以通过虚拟机配置调整缓存范围,其余类型则不可。

如果使用自动装箱机制初始化封装类,由于调用了valueOf()方法,导致使用缓存范围内的值时,则判断==或equal()时总为true,而利用new创建包装类对象时,则不会使用缓存。

装箱是通过包装类的ValueOf()方法实现的,拆箱是通过包装器的xxxValue()方法实现的。

  • 当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,
  • 而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)
  • 对于包装器类型,equals方法并不会进行类型转换

12.面向对象思想

类的关系

关联

关联是一种描述两个类之间行为的一般二元关系。

关联中的每个类可以指定一个数字或一个数字区间,表示该关联涉及类中的多少个对象。

在 Java 代码中,关联可以用数据域和方法进行实现,一个类中的方法包含另一个类的参数。

聚集和组合

聚集是一种特殊的关联形式,表示两个对象之间的所属关系。聚集模拟具有(has-a)关系。

所有者对象称为聚集对象,对应的类称为聚集类;从属对象称为被聚集对象,对应的类称为被聚集类。

一个对象可以被几个聚集对象所拥有。

如果一个对象被一个聚集对象所专有,该对象和聚集对象之间的关系就称为组合

依赖

依赖指两个类之间一个类使用另一个类的关系,前者称为客户(client),后者称为供应方(supplier)。

在 Java 代码中,实现依赖的方式是,客户类中的方法包含供应方类的参数。

下面的代码是依赖的例子。Java 自带的抽象类 Calendar 包含方法 setTime,该方法包含一个 Date 类型的参数,此处 Calendar 是客户类,Date 是供应方类。

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
    public final void setTime(Date date) {
        setTimeInMillis(date.getTime());
    }
}

继承

继承模拟是(is-a)关系。

  • 强是(strong is-a)关系描述两个类之间的直接继承关系。

  • 弱是(weak is-a)关系描述一个类具有某些属性。

  • 强是关系可以用类的继承表示,弱是关系可以用接口表示。

类的设计原则

高内聚,低耦合。

内聚性

同一个类/模块的所有操作应该有高度关联性,支持共同的目标,只负责一项任务,即单一责任原则,称为高内聚

不同类/模块之间的关联程度应该尽量低,对一个类/模块的修改应该尽量减少对其他类/模块的影响,称为低耦合

封装性

类中的数据域应该使用 private 修饰符隐藏其可见性,避免从外部直接访问数据域。

如果需要从外部读取数据域的值,则提供读取器方法。如果需要从外部修改数据域的值,提供设置器方法。

如果一个方法只在类的内部使用,则应该对该方法使用 private 修饰符,避免从外部调用该方法。

实例和静态

依赖于类的具体实例的数据域和方法应声明为实例数据域和实例方法,反之应声明为静态数据域和静态方法。

如果一个数据域被所有实例共享,该数据域应声明为静态数据域。如果一个方法不依赖于具体实例,该方法应声明为静态方法。

继承和聚集

继承模拟是(is-a)关系,聚集模拟具有(has-a)关系。

应考虑两个类之间的关系为是关系还是具有关系,决定使用继承或聚集。


13.序列化和反序列化

把对象转换成字节序列的过程称为对象的序列化,

把字节序列恢复成对象的过程称为对象的反序列化。

可序列化接口 Serializable

只有当一个类实现了 Serializable 接口时,这个类的实例才是可序列化的。

Serializable 接口是一个标识接口,用于标识一个对象是否可被序列化,该接口不包含任何数据域和方法。

如果试图对一个没有实现 Serializable 接口的类的实例进行序列化,会抛出 NotSerializableException 异常。

将一个对象序列化时,会将该对象的数据域进行序列化,不会对静态数据域进行序列化。

关键字 transient

如果一个对象的类实现了 Serializable 接口,但是包含一个不可序列化的数据域,则该对象不可序列化。为了使该对象可序列化,需要给不可序列化的数据域加上关键字 transient

如果一个数据域可序列化,但是不想将这个数据域序列化,也可以给该数据域加上关键字 transient

在序列化的过程中,加了关键字 transient 的数据域将被忽略。


14.反射机制

反射机制

Java 反射机制的核心是在程序运行时动态加载类以及获取类的信息,从而使用类和对象的数据域和方法。

Class 类

Class 类的作用是在程序运行时保存每个对象所属的类的信息,在程序运行时分析类。一个 Class 类型的对象表示一个特定类的属性。

有三种方法可以得到 Class 类型的实例。

  • 对一个对象调用 getClass 方法,获得该对象所属的类的 Class 对象。
  • 调用静态方法 Class.forName(),将类名作为参数,获得类名对应的 Class 对象。
  • 对任意的 Java 类型 T(包括基本数据类型、引用类型、数组、关键字 void),调用 T.class 获得类型 T 对应的 Class 对象,此时获得的 Class 对象表示一个类型,但是这个类型不一定是一种类。

三种方法中,通过静态方法 Class.forName 获得 Class 对象是最常用的。

Class 类的常用方法

Class 类中最常用的方法是 getName,该方法返回类的名字。

Class 类中的 getFieldsgetMethods getConstructors 方法分别返回类中所有的公有(即使用可见修饰符 public 修饰)的数据域、方法和构造方法。

Class 类中的 getDeclaredFieldsgetDeclaredMethods getDeclaredConstructors 方法分别返回类中所有的数据域、方法和构造方法(包括所有可见修饰符)。

  • getFields:获取类以及所有父类和父接口的public字段
  • getDeclaredFields:获取类本身的所有字段
  • getMethods:获取类以及所有父类和父接口的public方法
  • getDeclaredMethods:获取类本身的所有方法
  • getConstructors:获取类本身的public构造方法
  • getDeclaredConstructors:获取类本身的所有构造方法
  • setAccessible(true):暴力反射,可以无视权限控制,直接操作和修改private属性的值
posted @ 2022-05-27 09:25  萝卜不会抛异常  阅读(46)  评论(0编辑  收藏  举报