6.可见性修饰符和数据域封装
可见性修饰符包括 public
、private
、protected
和默认
修饰符
可见性修饰符 public
表示类成员可以在任何类中访问。
可见性修饰符 private
表示类成员只能从自身所在的类中访问。
如果不加任何可见性修饰符,则称为默认修饰符,表示类成员可以在同一个包里的任何类中访问,此时也称为包私有或包内访问。
可见性修饰符 protected
的可见性在 public 和默认之间,表示类成员可以在同一个包里的任何类中访问,也可以在该类的子类中访问。
子类可以覆盖父类的 protected 方法,并把该方法的可见性改成 public。但是子类不能降低父类方法的可见性,即不能把父类的 public 方法的可见性改成 protected。
数据域封装
可见性修饰符可以用于控制对类成员的访问,也可以用于对数据域进行封装。
数据域封装的含义是,对数据域使用 private 修饰符,将数据域声明为私有域。如果不使用数据域封装,则数据域的值可以从类的外部直接修改,导致数据被篡改以及类难以维护。使用数据域封装的目的是为了避免直接修改数据域的值。
7.字符串
常见的字符串类型包括 String、StringBuffer 和 StringBuilder。
- String 是不可变类型,每次对 String 的修改操作都会创建新的 String 对象,导致效率低下且占用大量内存空间,因此String 适用于字符串常量的情形,不适合需要对字符串进行大量修改的情形。
- StringBuffer 是可变类型,可以修改字符串的内容且不会创建新的对象,且 StringBuffer 是线程安全的,适用于多线程环境。
- 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,则要求子类的方法的返回值类型和父类的方法的返回值类型相同。如果方法的返回值类型是引用类型,则要求返回值类型相同或者子类的方法的返回值类型是父类的方法的返回值类型的子类。
实例方法只有当可访问时才能被重写。由于私有方法不能在定义该方法的类外访问,因此私有方法不能被重写。
静态方法可以被继承,但是不能被重写。
重写和重载的区别
重载指在同一个类中定义多个方法,这些方法有相同的名称,但是方法签名不同。
重写指在子类中定义一个方法,该方法与父类中的方法的签名相同,返回值类型相同或者子类的方法的返回值类型是父类的方法的返回值类型的子类。
下面的表格概括了重载和重写的区别。
关键字 final
关键字 final 可以用于声明常量,表示常量不会改变。
关键字 final 也可以用于修饰类和方法。使用 final 修饰的类是终极类,不能被继承。使用 final 修饰的方法不能被子类重写。
例如,String、StringBuffer 和 StringBuilder 类都使用了关键字 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.抽象类和接口
抽象类和接口的区别
- 抽象类的变量没有限制,接口只包含常量,即接口的所有变量必须是 public static final。
- 抽象类包含构造方法,子类通过构造方法链调用构造方法,接口不包含构造方法。
- 抽象类的方法没有限制,接口的方法必须是 public abstract 的实例方法。
- 一个类只能继承一个父类,但是可以实现多个接口。一个接口可以继承多个接口。
自定义比较方法
有两个接口可以实现对象之间的排序和比较大小。
Comparable 接口是排序接口。如果一个类实现了 Comparable 接口,则该类的对象可以排序。Comparable 接口包含一个抽象方法 compareTo,实现 Comparable 接口的类需要实现该方法,定义排序的依据。
Comparator 接口是比较器接口。如果一个类本身不支持排序(即没有实现 Comparable 接口),但是又需要对该类的对象排序,则可以通过实现 Comparator 接口的方式建立比较器。Comparator 接口包含两个抽象方法 compare 和 equals,其中 compare 方法是必须在实现类中实现的,而 equals 方法在任何类中默认已经实现。
如果需要对一个数组或列表中的多个对象进行排序,则可以将对象的类定义成实现 Comparable 接口,也可以在排序时定义 Comparator 比较器。
11.基本数据类型和包装类
包装类的构造方法
可以通过包装类的构造方法创建包装对象。调用构造方法时,构造方法的参数值可以是基本数据类型的值,也可以是表示值的字符串。
包装类的构造方法都是有参数的,没有无参数构造方法。
包装类的实例都是不可变的,一旦创建了包装对象,其内部的值就不能再改变。
自动装箱和自动拆箱
从 JDK 1.5 开始,基本数据类型和包装类之间可以进行自动转换。
将基本数据类型的值转换为包装对象,称为装箱。将包装对象转换为基本数据类型的值,称为拆箱。
12.面向对象思想
类的关系
关联
关联是一种描述两个类之间行为的一般二元关系。
关联中的每个类可以指定一个数字或一个数字区间,表示该关联涉及类中的多少个对象。
在 Java 代码中,关联可以用数据域和方法进行实现,一个类中的方法包含另一个类的参数。
聚集和组合
聚集是一种特殊的关联形式,表示两个对象之间的所属关系。聚集模拟具有(has-a)关系。
所有者对象称为聚集对象,对应的类称为聚集类;从属对象称为被聚集对象,对应的类称为被聚集类。
一个对象可以被几个聚集对象所拥有。如果一个对象被一个聚集对象所专有,该对象和聚集对象之间的关系就称为组合。
依赖
依赖指两个类之间一个类使用另一个类的关系,前者称为客户(client),后者称为供应方(supplier)。
在 Java 代码中,实现依赖的方式是,客户类中的方法包含供应方类的参数。
继承
继承模拟是(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 类中的 getFields、getMethods 和 getConstructors 方法分别返回类中所有的公有(即使用可见修饰符 public 修饰)的数据域、方法和构造方法。
Class 类中的 getDeclaredFields、getDeclaredMethods 和 getDeclaredConstructors 方法分别返回类中所有的数据域、方法和构造方法(包括所有可见修饰符)。
Class 类中的 getField、getMethod 和 getConstructor 方法分别返回类中单个的公有(即使用可见修饰符 public 修饰)的数据域、方法和构造方法。
Class 类中的 getDeclaredField、getDeclaredMethod 和 getDeclaredConstructor 方法分别返回类中单个的数据域、方法和构造方法(包括所有可见修饰符)。