代码的血脉:探讨Java中的继承与多态

简介: 代码的血脉:探讨Java中的继承与多态

相关概念

extends

关键字 extends 表明正在构造的新类派生于一个已存在的类。

已存在的类称为超类( superclass )、 基类( base class )或父类( parent class )。

新类称为子类( subclass )、派生类( derived class )或孩子类( child class )。

方法重写

子类可以重写父类的方法

super

super 关键字有两个用途:

  1. 调用超类的方法
  2. 调用超类的构造器

子类构造器

如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。 如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器。则 Java 编译器将报告错误。

public class Teacher extends People{
    private String num;
    public Teacher(String name, String sex, String age, String num) {
        super(name, sex, age);
        this.num = num;
    }
}

多态

一个对象变量(例如,变量 e )可以指示多种实际类型的现象被称为多态。在运行时能够自动地选择调用哪个方法的现象称为动态绑定。

对象变量是多态的,一个父类对象变量可以引用父类对象,也可以引用子类的对象。

强制类型转换

将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能够通过运行时的检査。

People people = new Teacher("张三", "男", 40, "1001");
Teacher teacher =  (Teacher) people;

抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看,祖先类更加通用,人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。

为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。抽象方法充当着占位的角色,它们的具体实现在子类中。

是否可以省略 Person 超类中的抽象方法,而仅在 Employee 和 Student 子类中定义 getDescription 方法呢?如果这样的话,就不能通过 Person 类的变量 p 调用 getDescription 方法了。编译器只允许调用在类中声明的方法。

受保护访问:proteced

在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected。

访问修饰符

修饰符 描述
private(私有) 仅对本类可见
public(公共) 对所有类可见
protected(受保护的) 对本包和所有子类可见
不声明修饰符(默认) 对本包可见

Object

Object 类是 Java 中所有类的始祖,在 Java 中每个类都是由它扩展而来的,可以使用 Object 类型的变量引用任何类型的对象。

Object类型的变量只能用于作为各种值的通用持有。在 Java 中,只有基本类型不是对象,例如,数值、 字符和布尔类型的值。

getClass

getClass 方法将返回一个对象所属的类。在检测中,只有在两个对象属于同一个类时,才有可能相等。

toString

用于返回表示对象值的字符串。

绝大多数(但不是全部)的 toString 方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。

随处可见 toString 方法的主要原因是:只要对象与一个字符串通过操作符 “+” 连接起来,Java编译就会自动地调用 toString 方法,以便获得这个对象的字符串描述。

如果没有重写 toString,那么默认会输出对象的的全路径类名和散列码,例如:java.io.PrintStream@2f6684。

toString 方法是一种非常有用的调试工具。在标准类库中,许多类都定义了 toString 方法,以便用户能够获得一些有关对象状态的必要信息。

@Override
public String toString() {
    return getClass().getName() + "{" +
            "num='" + num + '\'' + super.toString() +
            '}';
}

最好通过调用 getClass( ).getName( ) 获得类名的字符串,而不要将类名硬加到 toString 方法中。这样可以提供子类使用。

强烈建议为自定义的每一个类增加 toString 方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。

数组不能直接调用 toString( )。打印数组应使用 Arrays.toString(array);

int[] array = new int[]{1, 2, 3, 4, 5};
// 输出输出 [I@3cd1a2f1,前缀 [I表明是一个整型数组。
System.out.println(array);
// 打印输出[1, 2, 3, 4, 5]
System.out.println(Arrays.toString(array));

equals

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。

equals 比较的是对象的状态,== 比较的是对象的地址。

Object 的 equals 比较的地址,其他类如果不重写,也默认比较的是地址。

自反性

对于任何非空引用 x, x.equals(x) 应该返回 true。

对称性

对于任何引用 x 和 y,当且仅当 y.equals(x) 返回 true,x.equals(y) 也应该返回 true。

传递性

对于任何引用 x、 y 和 z,如果 x.equals(y) 返回 true,y.equals( z) 返回 true,那么 x.equals(z) 也应该返回 true。

一致性

如果 x 和 y 引用的对象没有发生变化,反复调用 x.equaIs(y) 应该返回同样的结果。

Null

对于任意非空引用 x,x.equals(null) 应该返回 false。

hashCode

散列码( hash code )是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是 两个不同的对象, x.hashCode( ) 与 y.hashCode( ) 基本上不会相同

由于 hashCode 方法定义在 Object 类中, 因此每个对象都有一个默认的散列码,其值为对象的存储地址。

hashCode 方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。

字符串的 hashCode 与内容有关,内容一样,则 hashCode 一样。

public int hashCode() {
  int h = hash;
  if (h == 0 && value.length > 0) {
    char val[] = value;
    for (int i = 0; i < value.length; i++) {
      h = 31 * h + val[i];
    }
    hash = h;
  }
  return h;
}

如果不重写 Object 类的 hashCode 方法,则都会使用对象地址作为散列码。

重写 equals 方法,同时必须重写 hashCode 方法。equals 与 hashCode 的定义必须一致: 如果 x.equals(y) 返回 true,那么 x.hashCode( ) 就必须与 y.hashCode( ) 具有相同的值。例如,如果用定义的 Employee.equals 比较雇员的 ID,那么 hashCode 方法就需要散列 ID,而不是雇员的姓名或存储地址。

对象包装器与自动装箱

所有的基本类型都冇一个与之对应的类,这些类称为包装器。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character 和 Boolean(前 6 个类派生于公共的超类 Number )。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是 final,因此不能定义它们的子类。

由于每个值分别包装在对象中, 所以 ArrayList 的效率远远低于 int[ ] 数组。

自动装箱

list.add(3),会自动的转换成 list.add(Integer.valueOf(3)),这种变换被称为自动装箱。装箱和拆箱是编译器认可的,而不是虚拟机。

包装器类引用可以为 null,自动装箱可能出现空指针。

如果在一个条件表达式中混合使用 Integer 和 Double 类型, Integer 值就会拆箱,提升为 double,再装箱为 Double。

自动装箱规范要求 boolean、byte、char,介于 -128 ~ 127 之间的 short 和int 被包装到固定的对象中。例如,如果在前面的例子中将 a 和 b 初始化为 100,对它们进行比较的结果一定成立。

自动拆箱

当将一个 Integer 对象赋给一个 int 值时,将会自动地拆箱。甚至在算术表达式中也能够自动地装箱和拆箱。

常用 API

参数数量可变的方法

可以用可变的参数数量调用的方法。

可以定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型。允许将一个数组传递给可变参数方法的最后一个参数。

public static int max(int... values) {
    int max = Integer.MIN_VALUE;
    for (int value : values) {
        if (value > max) max = value;
    }
    return max;
}

枚举类

在比较两个枚举类型的值时,永远不需要调用 equals,而直接使用 == 就可以了。

所有的枚举类型都是 Enum 类的子类。它们继承了这个类的许多方法。其中最有用的一 个是 toString, 这个方法能够返回枚举常量名。例如:Size.SMALL.toString( ) 将返回字符串 “SMALL”。

ordinal 方法返回 enum 声明中枚举常量的位置,位置从 0 开始计数。例如:Size.MEDIUM.ordinal( ) 返回 1。

常用 API

反射

能够分析类能力的程序称为反射。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:

  1. 在运行时分析类的能力
  2. 在运行时查看对象,例如:编写一个 toString 方法供所有类使用
  3. 实现通用的数组操作代码
  4. 利用 Method 对象,这个对象很像中的函数指针

Class类

在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。

保存这些信息的类被称为 Class,Object 类中的 getClass( ) 方法将会返回一个 Class 类型的实例。

最常用的 Class 方法是 getName。这个方法将返回类的名字。

获取Class对象的三种方式
  1. 对象.getClass( )
  2. Class.forName(“包名.类名”),应该使用 try catch 语句包裹
  3. 类型.class

一个 Class 对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int 不是类,但 int.class 是一个 Class 类型的对象。

鉴于历史 getName 方法在应用于数组类型的时候会返回一个很奇怪的名字:

  • Double[ ] class.getName( ),返回 “[Ljava.lang.Double; ’’
  • int[ ].class.getName( ),返回“[ I”
类对象比较:e.getClass() == Employee.class
使用类对象创建实例:newlnstance
// 此方法调用的是类默认的无参构造,如果不存在默认构造,则会抛出异常:java.lang.NoSuchMethodException()。
e.getClass().newlnstance() ;
// 将 forName 与 newlnstance 配合起来使用,可以根据存储在字符串中的类名创建一个对象。
Object obj = Class.forName("world.xuewei.entity.Student").newInstance();

分析类

Class 类中的 getFields、getMethods 和 getConstructors 方法将分别返回类提供的 public 域、方法和构造器数组, 其中包括超类的公有成员。 Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

反射类 描述
java.lang.reflect.Field Field 类有一 个 getType 方法,用来返回描述域所属类型的 Class 对象。
java.lang.reflect.Method Method 和 Constructor 类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。
java.lang.reflect.Constructor -

分别用于描述类的域、方法和构造器。 这三个类都有一个叫做 getName 的方法,用来返回项目的名称。这三个类还有一个叫做 getModifiers 的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。另外,还可以利用 java.lang.reflect 包中的 Modifieir 类的静态方法分析 getModifiers 返回的整型数值。 例如,可以使用 Modifier 类中的 isPublic、isPrivate 或 isFinal 判断方法或构造器是否是 public、private 或 final。

运行时分析对象

利用反射机制可以查看在编译时还不清楚的对象域。

查看对象域的关键方法是 Field 类中的 get方法。如果 f 是一个 Field 类型的对象(例如,通过 getDeclaredFields 得到的对象),obj 是某个包含 f 域的类的对象,f.get(obj) 将返回一个对象,其值为 obj 域的当前值。

get 方法还有一个需要解决的问题。name 域是一个 String,因此把它作为 Object 返回没有什么问题。但是,假定我们想要查看 salary 域。它属于 double 类型,而 Java 中数值类型不是对象。要想解决这个问题,可以使用 Field 类中的 getDouble 方法,也可以调用 get 方法,此时,反射机制将会自动地将这个域值打包到相应的对象包装器中,这里将打包成Double。

注意如果 get 的是私有域,需要使用 setAccessible 将域设置为可访问,setAccessible 方法是 AccessibleObject 类中的一个方法,它是 Field、Method 和 Constructor 类的公共超类。这个特性是为调试、持久存储和相似机制提供的。

任意类通用toString方法

使用 getDeclaredFileds 获得所有的数据域,使用 setAccessible 将所有的域设置为可访问,对于每一个域,获取名字和值,递归调用 toString,将每个值转为字符串。注意无限递归!

反射编写范型数组

一个对象数组不能转换成雇员数组( Employee[ ] )。如果这样做,则在运行时 Java 将会产生 ClassCastException 异常。前面已经看到,Java 数组会记住每个元素的类型,即创建数组时 new 表达式中使用的元素类型。将一个 Employee[ ] 临时地转换成 Object[ ] 数组,然后再把它转换回来是可以的,但从开始就是 Objectt ]的数组却永远不能转换成 Employe 数组。为了编写这类通用的数组代码,需要能够创建与原数组类型相同的新数组。为此,需要 java.lang.reflect 包中 Array 类的一些方法。其中最关键的是 Array 类中的静态方法 newlnstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。

正例

反例

反射调用任意方法,甚至私有

继承建议

将公共操作和域放在超类

这就是为什么将姓名域放在 Person 类中,而没有将它放在 Employee 和 Student 类中的原因。

不要使用受保护的域

子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问 protected 的实例域,从而破坏了封装性。

在同一个包中的所有类都可以访问 proteced 域,而不管它是否为这个类的子类。

使用继承实现“ is-a” 关系

除非所有继承的方法都有意义,否则不要使用继承

在覆盖方法时,不要改变预期的行为

使用多态,而非类型信息

不要过多地使用反射

反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。 这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。

笔记大部分摘录自《Java核心技术卷I》,含有少数本人修改补充痕迹。

相关文章
|
5天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
20天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
33 5
Java反射机制:解锁代码的无限可能
|
16天前
|
jenkins Java 测试技术
如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例详细说明
本文介绍了如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例,详细说明了从 Jenkins 安装配置到自动构建、测试和部署的全流程。文中还提供了一个 Jenkinsfile 示例,并分享了实践经验,强调了版本控制、自动化测试等关键点的重要性。
48 3
|
21天前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
60 10
|
17天前
|
分布式计算 Java MaxCompute
ODPS MR节点跑graph连通分量计算代码报错java heap space如何解决
任务启动命令:jar -resources odps-graph-connect-family-2.0-SNAPSHOT.jar -classpath ./odps-graph-connect-family-2.0-SNAPSHOT.jar ConnectFamily 若是设置参数该如何设置
|
15天前
|
Java
Java代码解释++i和i++的五个主要区别
本文介绍了前缀递增(++i)和后缀递增(i++)的区别。两者在独立语句中无差异,但在赋值表达式中,i++ 返回原值,++i 返回新值;在复杂表达式中计算顺序不同;在循环中虽结果相同但使用方式有别。最后通过 `Counter` 类模拟了两者的内部实现原理。
Java代码解释++i和i++的五个主要区别
|
23天前
|
搜索推荐 Java 数据库连接
Java|在 IDEA 里自动生成 MyBatis 模板代码
基于 MyBatis 开发的项目,新增数据库表以后,总是需要编写对应的 Entity、Mapper 和 Service 等等 Class 的代码,这些都是重复的工作,我们可以想一些办法来自动生成这些代码。
30 6
|
24天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
24天前
|
Java
通过Java代码解释成员变量(实例变量)和局部变量的区别
本文通过一个Java示例,详细解释了成员变量(实例变量)和局部变量的区别。成员变量属于类的一部分,每个对象有独立的副本;局部变量则在方法或代码块内部声明,作用范围仅限于此。示例代码展示了如何在类中声明和使用这两种变量。
|
24天前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
33 2