03 JAVA类
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中, 那么多个类无需再定义这些属性和行为,只要继承那个类即可。
- 继承的出现减少了代码冗余,提高了代码的复用性
- 继承的出现,更有利于功能的扩展
- 继承的出现让类与类之间产生了关系,提供了多态的前提
3.1 概述
在 Java 中,没有类就无法做任何事情。要想使用对象,就必须首先构造对象,并指定其初始状态,然后对对象应用方法。
3.1.1 概述
类是构造对象的模板或蓝图。由类构造对象的过程称为创建类的实例。
封装是将数据与行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域,操纵数据的过程称为方法。实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域,只能调用其他类提供的接口。
3.1.2 类之间关系
依赖、聚合(关联)、继承等
-
继承:is-a关系,一种用于表示特殊与一般的关系。在UML术语中,描述继承的关系称为泛化
-
实现:实现表示面向对象编程中类的接口实现
-
依赖:uses-a关系,一个类的方法操纵另一个类的对象,即为一个类依赖于另一个类。开发中应该尽量减少类之间的依赖关系,即降低类之间的耦合度;(方法内有一个其他类的对象)
-
关联:表示两个实体之间的关系。有两种类型的关联:组合与聚合
- 聚合:has-a关系,聚合关系意味着一个类的对象包含有另一个类的对象(类中含有一个其他类的对象)
- 组合:一个类是另一个类的组成部分
组合与聚合的区别:
聚合是一个类在逻辑上包含另一个类,但是所包含的类的实例是可以独立于第一个类在其上下文之外生存的,即它是可以被其他类引用的。如部门与员工
组合则是当主类不再存在时,依赖类也不再存在。如房子与房间
关系 | UML连接符 | 解释 |
---|---|---|
继承(泛化) | 箭头指向父类 | |
接口实现 | 箭头指向接口 | |
依赖 | 箭头指向被包含的对象 | |
关联 | 箭头指向被关联的对象 | |
聚合 | 菱形指向整体 | |
组合 | 菱形指向整体 |
3.2 继承
已存在的类称为超类、基类或父类;新派生出来的类称为子类、派生类。
- 子类继承了父类,就继承了父类的方法和属性
- 在子类中,可以使用父类中定义的方法和属性,也可以创建新的数据和方法
3.2.1 super()方法
子类含有与父类一样的方法,但是子类不能直接访问父类的私有域,必须要借助于公有的接口,如get*()方法。同时在调用时,要使用super关键字来进行调用。在 Java 类中使用super来调用父类中的指定操作:
- super可用于访问父类中定义的属性
- super可用于调用父类中定义的成员方法
- super可用于在子类构造器中调用父类的构造器
- super的追溯不仅限于直接父类
- super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
super不是一个对象的引用,不能将super赋予一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
3.2.2 子类构造器
由于子类继承了父类的属性和方法,所以在对子类对象进行初始化的时候,必须要对父类中的属性进行初始化操作。但是由于子类不能访问父类的私有域,就必须要调用父类的构造器对父类的私有域进行初始化,可以使用super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。
如果子类的构造器没有显式的调用超类构造器,则将自动地调用超类默认的无参数的构造器。所以对于父类来说,必须要有一个无参数的构造器。
3.2.3 final类
不允许扩展的类称为final类,即该类不能再被其他类继承,其内部的方法无法被覆盖。当一个类被声明为final时,其内部的方法也会自动成为final方法,但是不包括域。
将方法或类声明为final的目的是:确保他们不会在子类中改变语义。
3.2.4 抽象类
包含一个或多个抽象方法的类本身必须要被声明为抽象的。
抽象类本身除了抽象方法外,还可以包含具体的数据与具体的方法;同时,类即使不含有抽象方法,也可以声明为抽象类。
抽象类不能被实例化,即不能创建抽象类的对象。但是可以定义一个抽象类的对象变量,该变量的引用必须是非抽象子类的对象。
3.3 多态
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。就是**用父类的引用指向子类的对象。**
子类的每个对象也是超类的对象。即一个父类对象变量可以引用一个父类对象,也可以引用一个子类对象,如:
//从超类的角度看,体现了多态性
Parent parent = new Parent();
Parent parent = new Children();
//但是不能将一个超类的引用赋给子类变量
//Children child = new Parent();
3.3.1 概述
(1)特点
对象的多态——在 Java 中,子类的对象可以替代父类的对象使用。
-
一个变量只能有一种确定的数据类型
-
一个引用类型变量可能指向(引用)多种不同类型的对象
Person p = new Student(); Object o = new Person(); //Object类型的变量o, 指向Person类型的对象 o = new Student(); //Object类型的变量o, 指向Student类型的对象
-
子类可看做是特殊的父类, 所以父类类型的引用可以指向子类的对象:向上转型(upcasting)
-
一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问在子类中添加的属性和方法
Student m = new Student(); m.school = “pku”; //合法,Student类有school成员变量 Person e = new Student(); e.school = “pku”; //非法,Person类没有school成员变量
属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误
(2)虚拟方法调用
-
正常的方法调用
Person e = new Person(); // getInfo方法是父类中定义的共有方法 e.getInfo(); // 调用的是父类中的getInfo方法 Student e = new Student(); e.getInfo(); // 调用的是子类中的getInfo方法
-
虚拟方法调用
多态情况下,子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
Person e = new Student(); // 父类类型的引用指向一个子类的对象,编译时确定(静态绑定) e.getInfo(); // 父类调用子类Student类的getInfo()方法,运行时确定(动态绑定)
编译时类型和运行时类型:
编译时的类型是父类,而方法的调用是在运行时确定的,所以调用的是其中某一个子类重写的一个共有方法——动态绑定
3.3.2 重载与重写
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
(1)重载
重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重载对返回类型没有特殊的要求,只对方法名和参数列表有要求。
- 必须保证方法名相同
- 参数列表不相同【参数类型与个数,与参数名无关】
- 对返回类型不做要求,前提是参数列表也要不同
(2)重写
- 子类的重写方法首先要保证与父类的方法声明一致
- 子类重写方法的参数列表要与父类的参数列表一致。一旦子类中方法的参数列表改变了,那么该方法将会被视为该子类的成员方法
- 若返回类型是基本数据类型,那么子类中的返回类型必须与父类中的保持一致;若返回类型是引用类型,那么子类重写方法的数据类型可以与父类中的不相同,子类方法中的返回类型可以设置为父类返回类型的派生类
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected
- 父类的成员方法只能被它的子类重写
- 声明为 final 的方法不能被重写
- 声明为 static 的方法不能被重写,但是能够被再次声明
- 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法
- 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法
- 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以
- 构造方法不能被重写
- 如果不能继承一个类,则不能重写该类的方法
(3)总结
重写与重载都对方法的声明与参数列表有要求,而返回类型是根据参数列表的异同来确定是否有要求的。
- 重载:
- 参数列表相同时,编译出错
- 参数列表必须不相同,对返回类型不做要求
- 重写:
- 参数列表必须相同。若返回类型是基本数据类型,那么必须相同;若为引用数据类型,那么返回类型可以不同,但是子类中的返回类型必须是父类返回类型的派生类
- 参数列表不同,那么该方法将被视为子类的成员方法
3.4 Object类
3.4.1 概述
-
Object类是所有 Java 类的根父类
-
如果在类的声明中未使用extends关键字指明其父类, 则默认父类为java.lang.Object类
public class Person { ... } 等价于: public class Person extends Object { ... }
3.4.2 主要方法
(1)toString()
toString()方法在Object类中定义, 其返回值是String类型, 返回类名和它的引用地址。
-
在进行String与其它类型数据的连接操作时,自动调用toString()方法
Date now=new Date(); System.out.println(“now=”+now); // 相当于 System.out.println(“now=”+now.toString());
-
可以根据需要在用户自定义类型中重写toString()方法。如 String 类重写了toString()方法,就会返回字符串的字面值,如果某一个类没有默认重写toString()方法,那么就会返回它的类名与引用地址。
// 未重写toString()方法的自定义类型Person,返回类名@引用地址 Person p5 = new Person("zoip", 888); System.out.println(p5); //com.zdp.learn.studyCollection.Person@32a1bec0 // 重写toString()方法的自定义类型Person,返回对象的内容 Person p5 = new Person("zoip", 888); System.out.println(p5); //Person{name='zoip', age=888} // String默认重写了toString()方法 s1=“hello”; System.out.println(s1); //返回hello,相当于System.out.println(s1.toString());
-
基本类型数据转换为String类型时, 调用了对应包装类的toString()方法
int a=10; System.out.println(“a=”+a); // 数组是引用数据类型 public void test() { char[] arr = new char[] { 'a', 'b', 'c' }; System.out.println(arr); // abc int[] arr1 = new int[] { 1, 2, 3 }; System.out.println(arr1); // [I@1d8d30f7 double[] arr2 = new double[] { 1.1, 2.2, 3.3 }; System.out.println(arr2); // [D@3e57cd70 }
(2)equals()
…
(3)类生命周期相关方法
-
getClass()
-
finalize()
(4)线程通信相关方法
- notify()
- notifyAll()
- wait()
3.5 包装类
Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作, Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从 Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
3.5.1 对象包装
Java 是面向对象的语言,但并不是“纯面向对象”的,因为我们经常用到的基本数据类型就不是对象。但是我们在实际应用中经常需要将基本数据转化成对象,以便于操作。比如:将基本数据类型存储到Object[]数组或集合中的操作等等。
为了解决这个不足, Java 在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。包装类均位于java.lang包。
这些对象包装器类有:Integer,Long,Double,Byte,Character,Void,Boolean。对象包装器类是final的,不能定义他们的子类,即不可被继承。
//尖括号内必须为引用类型
ArrayList<Integer> list = new ArrayList<>();
//但是由于每个值都是被包装在对象中的,所以像这种情况,ArrayList的效率是远远低于int[]的效率
3.5.2 装箱与拆箱
//自动装箱
Integer total = 99;
//自动拆箱
int totalprim = total;
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
3.5.3 过程分析
以Integer为例:
public class Main {
public static void main(String[] args) {
//自动装箱
Integer total = 99;
//自动拆箱
int totalprim = total;
}
}
其内部具体的调用过程如下:
(1)调用函数
调用valueOf(),将int包装为Integer
- Integer total = 99;
执行上面那句代码的时候,系统为我们执行了: Integer total = Integer.valueOf(99);
调用intValue(),将Integer拆箱为int
- int totalprim = total;
执行上面那句代码的时候,系统为我们执行了: int totalprim = total.intValue();
(2)valueOf()函数
缓存:包装器内部预先保存有[-128,127]共256个数,是为了防止多次创建:
public static Integer valueOf(int i) {
//首先判断i的大小:
//如果i小于-128或者大于等于128,就创建一个Integer对象(使用构造器)
//否则执行SMALL_VALUES[i + 128]
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
(3)Integer()构造函数
private final int value;
public Integer(int value) {
this.value = value;
}
public Integer(String string) throws NumberFormatException {
this(parseInt(string));
}
定义了一个value变量,创建一个Integer对象,就会给这个变量初始化。第二个传入的是一个String变量,它会先把它转换成一个int值,然后进行初始化。
(4)SMALL_VALUES[]
private static final Integer[] SMALL_VALUES = new Integer[256];
它是一个静态的Integer数组对象,且已经被创建好了,其容量为256。
包装类的缓存问题:
因为对于Integer,在[-128,128)之间只有固定的256个值,所以为了避免多次创建对象,我们事先就创建好一个大小为256的Integer数组SMALL_VALUES[],所以如果值在这个范围内,就可以直接返回我们事先创建好的integer对象。
(5)缓存池
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
- new Integer(123) 每次都会新建一个对象;
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123);
Integer y = new Integer(123);
// 对象做相等操作,比较的是地址
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
基本类型对应的缓冲池如下:
- Boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。
在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。
(6)综述
在Integer的构造函数中,它分两种情况:
- i >= 128 || i < -128 => new Integer(i) :每次执行后,即使是相同数值的数所指向的对象也不一样
- i < 128 && i >= -128 => SMALL_VALUES[i + 128](数组 下标为0-255):每次执行后,相同数值的数所指向的对象一样
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2); //true
System.out.println(i3==i4); //false
}
}
/* i1和i2会进行自动装箱,执行了valueOf函数,它们的值在(-128,128]这个范围内,它们会拿到SMALL_VALUES数组里面的同一个对象SMALL_VALUES[228],它们引用到了同一个Integer对象,所以它们肯定是相等的。
*/
/* i3和i4也会进行自动装箱,执行了valueOf函数,它们的值大于128,所以会执行new Integer(200),也就是说它们会分别创建两个不同的对象,所以它们肯定不等。
*/
但是,对于浮点数而言,每次的结果都是不一样的。因为浮点数在这个范围内数的个数是无限的。所以对于浮点数的操作就很直接:
//每次都是新创建一个Double对象
public static Double valueOf(double d) {
return new Double(d);
}
- Integer派别:Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。
- Double派别:Double、Float的valueOf方法的实现是类似的。每次都返回不同的对象。