Java面向对象(中)
面向对象(中)
面向对象特征之二: 继承性
一、继承性的好处:
- 减少了代码的冗余,提高了代码的复用性
- 便于功能的扩展
- 为之后多态性的使用,提供了前提
二、继承性的格式:class A extends B{}
- extends:延展、扩展
- A:子类、派生类、subclass
- B:父类、超类、基类、superclass
-
- 体现:一旦子类A继承父类B以后,子类A中就获取了父类B中声明的所有的属性和方法
- 特别的,父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私有的结构。
- 只是因为封装性的影响,使得子类不能直接调用父类的结构而已。
-
- 子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的扩展。
- 子类和父类的关系,不同于子类和集合的关系。
三、Java中关于继承性的规定:
- 一个类可以被多个子类继承。
- Java中类的单继承性:一个类只能有一个父类
- 子父类是相对的概念:A类是B类的子类,也可以是C类的父类
- 子类直接继承的父类,称为:直接父类。间接继承的父类称为:间接父类
- 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法
四
- 如果我们没有显式的声明一个类的父类的话,则此类继承于Java.lang.Object类
- 所有的Java类(除Java.lang.Object类之外)都直接或间接的继承于Java.lang.Object类
- 意味着,所有的Java类具有Java.lang.Object类声明的功能。
方法的重写 (override/overwrite)
-
重写:子类继承父类以后,可以对父类中同名同参的方法,进行覆盖操作
-
应用:重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的时子类重写父类的方法。
-
重写的规定:
- 约定俗称:子类中的叫重写的方法,父类中叫被重写的方法
- 子类重写的方法的方法名和形参列表与父类被重写的方法名和形参列表相同
- 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符
- 特殊情况:子类不能重写父类中声明为private权限的方法
- 返回值类型:
- 父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void
- 父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类
- 父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型
- 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型
-
注:子类和父类的同名同参数的方法要么都声明为非static的(考虑重写),要么都声明为static的(不是重写)。
-因为静态的方法无法被覆盖,随着类的加载而加载
关键字:super
- super理解为:父类的
- super可以用来调用:属性、方法、构造器
- super的使用:调用属性和方法
- 我们可以在子类的方法或构造器中。通过使用"super.属性"或"super.方法"的方式,显式的调用父类中声明的属性或方法。但是,通常情况下,我们习惯省略"super."
- 特殊情况:当子类和父类都定义了同名的属性时,我们要在子类中调用父类中的属性,则必须显示的使用"super.属性"的方式,表明调用的是父类中声明的属性。
- 特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法。
- super调用构造器
- 我们可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定的构造器
- "super(形参列表)"的使用,必须声明在子类构造器的首行! this(形参列表)也必须声明在首行
- 我们在类的构造器中,针对于"this(形参列表)"或"super(形参列表)"只能二选一,不能同时出现
- 在构造器的首行,没有显式的声明"this(形参列表)“或"super(形参列表)”,则默认调用的是父类中空参的构造器:super();
- 在类的多个构造器中,至少有一个类的构造器中使用了"super(形参列表)",调用父类中的构造器
this和super的区别
子类对象实例化的全过程
- 从结果上来看:(继承性)
- 子类继承父类以后,就获取了父类中声明的属性或方法。
- 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。
- 从过程上来看:
- 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,…直到调用了Java.lang.Object类中的空参构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,才可以考虑进行调用。
- 明确:虽然创建子类对象时,调用了父类的构造器,但时自始至终就创建了一个对象,即为new的子类对象。
面向对象特征之三: 多态性
-
多态性,是面向对象中最重要的概念,在Java中的体现:
- 对象的多态性:父类的引用指向子类的对象
-
Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译时,看左边;运行时,看右边。
-
若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
-
多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
-
-
多态的使用:虚拟方法调用
- 有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法。
- 总结:编译,看左边;运行,看右边。
-
多态性的使用前提:
- 类的继承关系
- 方法的重写
-
对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)
补充:
- 多态性也遵循就近原则,用子类创建的子类对象调用与父类同名的属性时,这里就没有多态性了(就等于是自己调自己)。但当子类中没有找到的时候,便会去最近的父类找
例:
package exer;
/*
* 练习:
* 1.若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,
* 系统将不可能把父类里的方法转移到子类中:编译看左边,运行看右边
*
* 2.对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,
* 这个实例变量依然不可能覆盖父类中定义的实例变量:编译运行都看左边
*
*
*/
class Base {
int count = 10;
public void display() {
System.out.println(this.count);
}
}
class Sub extends Base {
int count = 20;
public void display() {
System.out.println(this.count);
}
}
public class FieldMethodTest {
public static void main(String[] args) {
Sub s = new Sub();
System.out.println(s.count);// 20
s.display();// 20
// 引用数据类型赋值就是赋地址值
Base b = s;// 多态性
// ==:对于引用数据类型来讲,比较的是两个引用数据类型变量的地址值是否相同
System.out.println(b == s);// true
System.out.println(b.count);// 10
//这里20是因为多态性调用了子类的方法,然后子类的方法调用了子类的属性
b.display();// 20
}
}
多态性其实在main()方法中没什么好处,就是用父类的引用调用子类的方法,而且只能调用父类中的方法和属性,如果子类有重写的方法就会覆盖掉父类的方法。但无法调用子类特有的方法。综上,多态调用的只能是父类的特有方法、属性和子类重写的方法。
但多态真正用的地方是在形参部分,只需要写一个父类的,然后调用的时候需要它子类的哪个功能就可以直接调用那个内容,就比如数据库连接、抽象类和接口
面试题
谈谈你对多态性的理解?
-
实现代码的通用性
-
Object类中定义的public boolean equals(Object obj){ }
JDBC:使用Java程序操作(获取数据库连接、CRUD)数据库(MySQL、Oracle、DB2、SQL Server)
-
抽象类、接口的使用肯定体现了多态性。(抽象类、接口不能实例化)
多态是编译时行为还是运行时行为?
证明如下:
package java5;
//解释多态性是运行时行为
import java.util.Random;
//面试题:多态是编译时行为还是运行时行为?
//证明如下:
class Animal {
protected void eat() {
System.out.println("animal eat food");
}
}
class Cat extends Animal {
protected void eat() {
System.out.println("cat eat fish");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Sheep extends Animal {
public void eat() {
System.out.println("Sheep eat grass");
}
}
public class InterviewTest {
public static Animal getInstance(int key) {
switch (key) {
case 0:
return new Cat();
case 1:
return new Dog();
default:
return new Sheep();
}
}
public static void main(String[] args) {
int key = new Random().nextInt(3);
System.out.println(key);
Animal animal = getInstance(key);
animal.eat();
}
}
例二:笔试题
package exer;
//考查多态的笔试题目:
public class InterviewTest1 {
public static void main(String[] args) {
Base1 base = new Sub1();
base.add(1, 2, 3); //sub_1
Sub1 s = (Sub1)base;
s.add(1,2,3); //sub_2
}
}
class Base1 {
public void add(int a, int... arr) {
System.out.println("base1");
}
}
class Sub1 extends Base1 {
public void add(int a, int[] arr) {
System.out.println("sub_1");
}
public void add(int a, int b, int c) {
System.out.println("sub_2");
}
}
方法的重载与重写
- 从编译和运行的角度看:
- 重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。
- 所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法, 这称为==“早绑定”或“静态绑定”==;
- 而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为==“晚绑定”或“动态绑定”==。
- 引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
向下转型
问题导入:
- 有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。如何才能调用子类特有的属性和方法?
向下转型:使用强制类型转换符。
Person p2 = new Man();
Man m1 = (Man) p2;
m1.earnMoney();
由上图可知,向下转型是多态的逆过程
注意:使用强转时,可能出现ClassCastException的异常。
几种情况:
编译时通过,运行时不通过
//举例一
Person p4 = new Woman();
Man m4 = (Man)p4;
//举例二
Person p5 = new Person();
Man m5 = (Man)p5;
编译通过,运行时也通过
Object obj = new Woman();
Person p = (Person)obj;
编译不通过
Man m6 = new Woman();
instanceof关键字的使用
为了在向下转型时不出现ClassCastException的异常,引入了instanceof关键字
-
a instanceof A
:判断对象a是否是类A的实例。如果是,返回true;如果不是,返回false。-
Person p2 = new Man(); if (p2 instanceof Woman) { // false Woman w1 = (Woman) p2; w1.goshopping(); System.out.println("*******Woman*******"); }
-
-
如果a instanceof A返回true,且类B时类A的父类,则a instanceof B也返回true。
- 意思就是,当a instanceof A是true,那么A的父类也都会返回true
-
要求a所属的类与类A必须是子类和父类的关系,否则编译错误。
- 比如说直接拿person的实例来instanceof不相关的类String,那么会直接报错
Object类的使用
- Object类是所有Java类的根父类
- 如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object类
- Object类中的功能(属性、方法)就具有通用性。
- Object类中只声明了一个空参的构造器
方法名 | 返回值类型 | 描述 |
---|---|---|
clone() | protected Object | 创造并返回当前复制的对象,直接对象.clone()进行调用,返回的类型要进行强转 |
equals(Object obj) | boolean | 指示一些其他对象是否等于此对象 |
finalize() | protected void | 当垃圾收集确认不再有对该对象的引用时,垃圾收集器在对象上调用此方法(这个方法一般不会自己主动去调用,一般是垃圾回收机制自动调用) |
getClass() | class<?> | 获取当前对象的所属类(返回此对象的类名) |
hashCode() | int | 返回对象的hash值(哈希码值) |
toString() | String | 返回对象的字符串形式 |
notify() | void | 唤醒正在等待对象监视器的单个线程 |
notifyAll() | void | 唤醒正在等待对象监视器的所有线程 |
wait() | void | 导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll() 方法。 |
wait(long timeout) | void | 导致当前线程等待,直到另一个线程调用 notify() 方法或该对象的 notifyAll() 方法,或者指定的时间已过。 |
wait(long timeout, int nanos) | void | 导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll() 方法,或者某些其他线程中断当前线程,或一定量的实时时间。 |
- 数组也作为Object类的子类出现,可以调用Object类中声明的方法
import org.junit.Test;
public class ReviewTest {
//数组也作为Object类的子类出现,可以调用Object类中声明的方法
@Test
public void test1() {
int[] arr = new int[] { 1, 2, 3 };
print(arr);// [I@443b7951
System.out.println(arr.getClass()); //class [I
System.out.println(arr.getClass().getSuperclass());// class java.lang.Object
}
public void print(Object obj) {
System.out.println(obj);
}
}
面试题
final、finally、finalize的区别?
== 和 equals() 区别
== 和 equals() 区别
一、回顾 == 的使用:
- == : 运算符
- 可以使用在基本数据类型变量和引用数据类型变量中
- 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。 (不一定类型要相同)
- 因为这里会有自动类型提升
- 如果比较的是引用数据类型变量:比较两个对象的地址值是否相同.即两个引用是否指向同一个对象实体
补充:= = 符号使用时,必须保证符号左右两边的变量类型一致。
二、equals() 的使用
-
是一个方法,而非运算符
-
只能适用于引用数据类型(因为基本数据类型无法调用方法,引用数据类型都是类的实例化,可以调用方法)
-
Object类中的equals()的定义:
-
public boolean equals(Object obj) { return (this == obj); }
说明:Object类中定义的equals()和 == 的作用是相同的:比较两个对象的地址值是否相同。即两个引用是否指向同一个对象实体
-
-
像String、Date、File、包装类等都重写了Object类中的equals()方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的"实体内容"是否相同。
-
通常情况下,我们自定义的类如果使用equals()的话,也通常是比较两个对象的"实体内容"是否相同。那么,我们就需要对Object类中的equals()进行重写,重写的原则:比较两个对象的实体内容是否相同。
public class EqualsTest {
public static void main(String[] args) {
// 基本数据类型
int i = 10;
int j = 10;
double d = 10.0;
System.out.println(i == j);// true
System.out.println(i == d);// true
char c = 65;
char c1 = 'A';
char c2 = 10;
System.out.println(c); // A
System.out.println(c == c1);// true
System.out.println(c2 == i);// true
// 引用数据类型
Customer cust1 = new Customer("Tom", 21);
Customer cust2 = new Customer("Tom", 21);
System.out.println(cust1 == cust2); // false
String str1 = new String("cxdthd");
String str2 = new String("cxdthd");
System.out.println(str1 == str2);// false
System.out.println("******************************");
System.out.println(cust1.equals(cust2));// false,重写后为true
System.out.println(str1.equals(str2));// true
Date date1 = new Date(123);
Date date2 = new Date(123);
System.out.println(date1.equals(date2));// true
}
}
如何在自己创建的类中重写equlas()方法?
-
参考String类中的equals()方法
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; //这里的value指的是当前对象(this)的字符串长度,因为String类型在底层是以char型数组的形式存在 int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
-
手动实现equals()方法
//重写的原则:比较两个对象的实体内容(即:name和age)是否相同 //手动实现 @Override public boolean equals(Object obj) { //第一步,比较两个对象的地址值是否相等 if (this == obj) { return true; } //第二步,判断对象是否是this对象的实例 if (obj instanceof Customer) { // 方法一 // if (this.name.equals(((Customer) obj).name) && this.age == // ((Customer) obj).age) { // return true; // } // 方法二 return this.name.equals(((Customer) obj).name) && this.age == ((Customer) obj).age; } return false; }
-
Eclipse自动实现equals()方法重写
// 自动生成的equals()方法 @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Customer other = (Customer) obj; if (age != other.age) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; }
区别手动写的自动生成的equals()
@Test
public void test2(){
Person p = new Person("Tom",18);
Man m = new Man("Tom",18);
System.out.println(p.equals(m));//true ---> false
System.out.println(p.getClass());
}
- 上面的例子用自己写的equals()方法返回true,自动生成的equals()方法返回false
- 原因:在自己的方法中是用instanceof进行判断,Man是Person的子类会返回true从而进入if判断,而里面的实体内容又是相同的,所以会返回true;而在自动生成的equals()方法里,判断语句为getClass() != obj.getClass(),这样的话就是判断类是否相同,没法钻空子
toString() 方法
-
当我们输出一个对象的引用时,实际上就是调用当前对象的toString()
-
Object类中toString()的定义:
-
public String toString() { //返回他的hashCode值,并转换为十六进制 return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
-
-
像String、Date、File、包装类都重写了Object类中的toString()方法。使得在调用对象的toString()时,返回"实体内容"信息
-
自定义类也可以重写toString()方法,当调用此方法时,返回对象"实体内容"。
注意点:
@Test
public void test3(){
String s = "abc";
System.out.println(s);//abc
System.out.println(s.toString());//abc
s = "null";
System.out.println(s);//null为字符串
System.out.println(s.toString());//null
s = null;
System.out.println(s); //null为空
System.out.println(s.toString());//NullPointerException
}
- 当输出的内容为null(非字符串)时,调用toString()方法会出现空指针异常,本来输出的内容会默认调用它的toString()方法,这里12行没出现异常的原因是因为println()方法里的保护机制,如果s是null,就直接以null字符串的形式输出而不调用toString()方法,如下:
public void print(String s) {
if (s == null) {
s = "null";
}
write(s);
}
包装类的使用
- 针对八种基本数据类型定义相应的引用类型—包装类(封装类)
- 有了类的特点,就可以调用类中的方法,Java才是真正的面向对象
基本数据类型包 —>包装类 —装箱
-
调用包装类的构造器
-
int num1 = 10; Integer integer = new Integer(num1); Integer integer2 = new Integer("123"); //字符串形式也能自动识别 //错误写法,会报异常:NumberFormatException Integer integer3 = new Integer("123abc"); //只能纯数字,不能有其他符号 Float float1 = new Float(12.3f); Float float2 = new Float("12.3"); Boolean boolean1 = new Boolean(true); // true Boolean boolean2 = new Boolean("tRue"); // true:忽略大小写 Boolean boolean3 = new Boolean("abc"); // false
-
包装类 —>基本数据类型 —拆箱
-
调用包装类Xxx的
xxxValue()
-
Integer in1 = new Integer(12); int number1 = in1.intValue(); Float f1 = new Float(12.3); float f2 = f1.floatValue();
-
JDK 5.0新特性:自动装箱与自动拆箱
// 自动装箱:不需要新new一个构造器,可以直接赋给对象
// 基本数据类型 --->包装类
int num2 = 10;
Integer in1 = num2;
boolean b1 = true;
Boolean b2 = b1;
// 自动拆箱:不需要调用包装类,也是直接赋值给基本数据类型
// 包装类 --->基本数据类型
int num3 = in1;
基本数据类型、包装类 —>String类型
- 直接用
""
做连接运算就可以转为String的
// 方式一:连接运算
int num1 = 10;
String str1 = num1 + "";
System.out.println(str1 + 1);// 101
- 调用String重载的
valueOf(Xxx xxx)
// 方式二:调用String重载的valueOf(Xxx xxx)
float f1 = 12.3f;
String str2 = String.valueOf(f1);
System.out.println(str2 + 1);// 12.31
Double d1 = new Double(12.4);
String valueOf = String.valueOf(d1);
String类型—>基本数据类型、包装类
- 调用包装类的
parseXxx(String s)
方法
String str1 = "123";
System.out.println(str1 + 1);// 1231
// 错误的情况:只有子父类关系的情况下才可以使用强转
// int i1 = (int)str1;
// Integer integer = (Integer)str1;
// 两步:先转成Integer包装类,再转成int型(自己写的)
Integer integer = new Integer(str1);
int i1 = integer;
System.out.println(i1 + 1);// 124
// 一步到位:调用包装类的parseXxx(String s)方法
int parseInt = Integer.parseInt(str1); //这里str1如果有数字以外的内容,会报异常:NumberFormatException
System.out.println(parseInt + 2);// 125
String str2 = "trUe";
boolean parseBoolean = Boolean.parseBoolean(str2);
System.out.println(parseBoolean);// true
注意:
boolean型变量的默认初始化值为false,Boolean是一个包装类,所有没new对象的时候是个null
class Order {
boolean isMale;
Boolean isFemale;
}
@Test
public void test(){
Order order = new Order();
System.out.println(order.isMale); //false
System.out.println(oeder.isFemale); //null
}
关于包装类使用的面试题
例一
@Test
public void test1() {
// 三元运算要求数据类型要统一,所以int型会自动类型提升为double型
Object o1 = true ? new Integer(1) : new Double(2.0);
System.out.println(o1);// 1.0
}
@Test
public void test2() {
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
System.out.println(o2);// 1
}
例二
@Test
public void test3() {
Integer i = new Integer(1);
Integer j = new Integer(1);
//这里比较地址值
System.out.println(i == j);// false
// Integer内部定义了IntegerCache结构,IntegerCache中定义了Integer[],
// 保存了从-128~127范围的整数。如果我们使用自动装箱的方式,给Integer赋值的范围在
// -128~127范围内时,可以直接使用数组中的元素,不用再去new了。目的:提高效率
// 超出这个范围就需要新new对象,所以128为false
Integer m = 1;
Integer n = 1;
System.out.println(m == n);// true
Integer x = 128;// 相当于new了一个Integer对象
Integer y = 128;// 相当于new了一个Integer对象
System.out.println(x == y);// false
}
Integer.class里内部类IntegerCache的源码
private static class IntegerCache {
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;
}
private IntegerCache() {}
}