相关概念
extends
关键字 extends 表明正在构造的新类派生于一个已存在的类。
已存在的类称为超类( superclass )、 基类( base class )或父类( parent class )。
新类称为子类( subclass )、派生类( derived class )或孩子类( child class )。
方法重写
子类可以重写父类的方法
super
super 关键字有两个用途:
- 调用超类的方法
- 调用超类的构造器
子类构造器
如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。 如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器。则 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
反射
能够分析类能力的程序称为反射。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:
- 在运行时分析类的能力
- 在运行时查看对象,例如:编写一个 toString 方法供所有类使用
- 实现通用的数组操作代码
- 利用 Method 对象,这个对象很像中的函数指针
Class类
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。
保存这些信息的类被称为 Class,Object 类中的 getClass( ) 方法将会返回一个 Class 类型的实例。
最常用的 Class 方法是 getName。这个方法将返回类的名字。
获取Class对象的三种方式
- 对象.getClass( )
- Class.forName(“包名.类名”),应该使用 try catch 语句包裹
- 类型.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》,含有少数本人修改补充痕迹。