代码的血脉:探讨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》,含有少数本人修改补充痕迹。

相关文章
|
3天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
18 3
|
29天前
|
Java
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
51 24
|
11天前
|
前端开发 Java 测试技术
java日常开发中如何写出优雅的好维护的代码
代码可读性太差,实际是给团队后续开发中埋坑,优化在平时,没有那个团队会说我专门给你一个月来优化之前的代码,所以在日常开发中就要多注意可读性问题,不要写出几天之后自己都看不懂的代码。
49 2
|
25天前
|
Java 编译器 数据库
Java 中的注解(Annotations):代码中的 “元数据” 魔法
Java注解是代码中的“元数据”标签,不直接参与业务逻辑,但在编译或运行时提供重要信息。本文介绍了注解的基础语法、内置注解的应用场景,以及如何自定义注解和结合AOP技术实现方法执行日志记录,展示了注解在提升代码质量、简化开发流程和增强程序功能方面的强大作用。
65 5
|
25天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
49 5
|
27天前
|
Java API 开发者
Java中的Lambda表达式:简洁代码的利器####
本文探讨了Java中Lambda表达式的概念、用途及其在简化代码和提高开发效率方面的显著作用。通过具体实例,展示了Lambda表达式如何在Java 8及更高版本中替代传统的匿名内部类,使代码更加简洁易读。文章还简要介绍了Lambda表达式的语法和常见用法,帮助开发者更好地理解和应用这一强大的工具。 ####
|
1月前
|
Java API Maven
商汤人像如何对接?Java代码如何写?
商汤人像如何对接?Java代码如何写?
39 5
|
1月前
|
Java
在Java中,接口之间可以继承吗?
接口继承是一种重要的机制,它允许一个接口从另一个或多个接口继承方法和常量。
96 1
|
1月前
|
Java
在Java中实现接口的具体代码示例
可以根据具体的需求,创建更多的类来实现这个接口,以满足不同形状的计算需求。希望这个示例对你理解在 Java 中如何实现接口有所帮助。
87 38
|
24天前
|
安全 Java API
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。