一、面向对象 (OOP) 特征二:继承性
1. 继承性的理解
- 继承性的好处
- 减少了代码的冗余,提高了代码的复用
- 便于功能的扩展
- 为之后的多态性的使用提供了前提
-
public class Person { int age; String name; public Person() { } public Person(int age, String name) { this.age = age; this.name = name; } public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } }
-
public class Student extends Person { // int age; // String name; String major; public Student() { } public Student(int age, String name, String major) { this.age = age; this.name = name; this.major = major; } // public void eat() { // System.out.println("吃饭"); // } // public void sleep() { // System.out.println("睡觉"); // } public void study() { System.out.println("学习"); } }
-
public class PersonTest { public static void main(String[] args) { //创建Student类的对象 Student student = new Student(); student.eat(); //输出吃饭 student.study(); //输出学习 }
2. 继承性的使用
- 继承性的格式:class A extends B { }
- A类:子类、派生类、subclass
- B类:父类、超类、基类、superclass
- 体现:一旦子类A继承父类B以后,子类A中就获取了父类B中声明的所有的属性和方法
- 特别地,父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私有的结构,只因为封装性的影响,使得子类不能直接调用父类的结构而已
- 除此之外,子类继承父类以后,还可以声明自己特有的属性或方法,实现功能的扩展
- 子类和父类的关系,不同于子集和集合的关系
3. 继承性的再说明
- 一个类可以被多个子类继承
- Java中类的单继承性:一个类只能有一个父类
- 子父类是相对的概念,可以多层继承
- 子类直接继承的父类称为直接父类,间接继承的父类称为间接父类
- 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法
4. Object类的理解
- 如果没有显式地声明一个类的父类的话在,则此类继承于java.lang.Object类
- 所有的Java类 (除java.lang.Object类之外),都直接或间接地继承于java.lang.Object类
- 这意味着所有的Java类都具有java.lang.Object类声明的功能
二、方法的重写 (override/overwrite)
1. 方法重写的理解
- 定义:子类继承父类以后,可以对父类中同名同参的方法进行覆盖操作
- 应用:重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参的方法时,实际执行的时子类重写父类的方法
-
public class Person { int age; String name; public Person() { } public Person(int age, String name) { this.age = age; this.name = name; } public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } public void walk(int distance) { System.out.println("走路,走的距离是:" + distance + "公里"); } }
-
public class Student extends Person { String major; public Student() { } public Student(String major) { this.major = major; } //对父类中的eat()方法进行重写 public void eat() { System.out.println("学生多吃有营养的食物"); } public void study() { System.out.println("学习"); } }
-
public class PersonTest { public static void main(String[] args) { //创建Person类的对象 Person person = new Person(); person.eat(); //输出吃饭 //创建Student类的对象 Student student = new Student(); student.eat(); //输出学生多吃有营养的食物 }
2. 方法重写的细节
- 规定
- 方法的声明:权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型 { }
- 约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法
- 子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同
- 子类重写的方法的权限修饰符,不小于父类被重写的方法的权限修饰符
- 子类不能重写父类中声明为private权限的方法
- 返回值类型
- 父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void
- 父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类型或者A类型的子类
- 父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型
- 异常的类型
- 子类重写的方法抛出的异常类型,不大于父类被重写的方法抛出的异常类型 (具体放到异常处理部分讲)
- 子类和父类中同名同参的方法,要么都声明为非static的 (考虑重写),要么都声明为static的 (不是重写)
- 区分方法的重载和重写
- 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性
- 重载发生在一个类中,同名的方法如果有不同的参数列表 (参数类型不同、参数个数不同或者二者都不同) 则视为重载
- 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常 (里氏代换原则)
- 重载对返回类型没有特殊的要求,不能根据返回类型进行区分
- 重载可以理解为“早绑定”或“静态绑定”,重写可以理解为“晚绑定”或“动态绑定”
-
区别点 重载方法 重写方法 参数列表 必须修改 一定不能修改 返回类型 可以修改 一定不能修改 异常 可以修改 可以减少或删除,一定不能抛出新的或者更广的异常 访问 可以修改 一定不能做出更严格的限制(可以降低限制)
三、4种访问权限修饰符
- Java的权限修饰符:public、protected、缺省、private
- 置于类的成员定义前,用来限定对象对该类成员的访问权限
- 对于class的权限修饰符只可以用public和default (缺省)
- public类可以在任意地方被访问
- default类只可以被同一个包内部的类访问
四、关键字:super
1. super调用属性和方法
- super理解为:父类的...
- super可以用来调用:属性、方法和构造器
- 我们可以在子类的方法或构造器中,通过使用“super.属性”或“super.方法”的方式,显式地调用父类中声明的属性或方法,但是,通常情况下,我们习惯省略“super.”
- 特殊情况:当子类和父类中定义了同名的属性时,我们想要在子类中调用父类中声明的属性,则必须显式地使用“super.属性”的方式,表明调用的是父类中声明的属性
- 特殊情况:当子类和重写了父类中的方法以后,我们想要在子类的方法中调用父类中被重写的方法时,则必须显式地使用“super.方法”的方式,表明调用的是父类中声明的方法
-
public class Person { String name; int age; int id = 1; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } }
-
public class Student extends Person { String major; int id = 2; public Student() { } public Student(String major) { this.major = major; } //对父类中的eat()方法进行重写 public void eat() { System.out.println("学生多吃有营养的食物"); } public void study() { System.out.println("学习"); eat(); super.eat(); sleep(); } public void show() { System.out.println("id = " + id); System.out.println("id = " + super.id); } }
-
public class SuperTest { public static void main(String[] args) { //创建Student类的对象 Student student = new Student(); student.show(); //输出id = 2\nid = 1 student.study(); //输出学习\n学生多吃有营养的食物\n吃饭\n睡觉 }
2. super调用构造器
- 我们可以在子类的构造器中显式地使用“super(形参列表)”的方式,调用父类中声明的指定的构造器
- “super(形参列表)”的使用,必须声明在子类构造器的首行
- 我们在类的构造器中,针对于“this(形参列表)”或“super(形参列表)”只能二选一,不能同时出现
- 在构造器的首行,没有显式地声明“this(形参列表)”或“super(形参列表)”,则默认调用的是父类中空参的构造器
- 在类的多个构造器中,至少有一个类的构造器使用了“super(形参列表)”,调用父类的构造器
-
public class Person { String name; int age; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } }
-
public class Student extends Person { String major; public Student() { } public Student(String major) { this.major = major; } public Student(String name, int age, String major) { //this.name = name; //this.age = age; super(name, age); this.major = major; } //对父类中的eat()方法进行重写 public void eat() { System.out.println("学生多吃有营养的食物"); } public void study() { System.out.println("学习"); } public void show() { System.out.println("name是" + name + ",age是" + age + ",major是" + major); } }
-
public class SuperTest { public static void main(String[] args) { //创建Student类的对象 Student student = new Student("Tom", 25, "IT"); student.show(); //输出name是Tom,age是25,major是IT }
五、子类对象实例化过程
- 从结果上看 (继承性)
- 子类继承父类以后,就获取了父类中声明的属性或方法
- 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性
- 从过程上看
- 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接地调用其父类的构造器,进而调用父类的父类的构造器,...直到调用了java.lang.Object类中空参的构造器为止,正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用
- 虽然创建子类对象时,调用了父类的构造器,但是自始至终只创建过一个对象,即new的子类对象
六、面向对象 (OOP) 特征三:多态性
1. 多态性的使用
- 理解多态性:一个事物的多种形态
- 何为多态性:对象的多态性,父类的引用指向子类的对象 (子类的对象赋给父类的引用)
- 使用多态性:虚拟方法调用
- 有了对象的多态性以后,在编译期只能调用父类中声明的方法,在运行期实际执行的是子类中重写父类的方法,即编译看左边,运行看右边
- 当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法
- 多态性的使用前提
- 类的继承关系
- 方法的重写
-
public class Person { String name; int age; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } }
-
public class Man extends Person { boolean isStrong; //对父类中的eat()方法进行重写 public void eat() { System.out.println("男人饭量大"); } public void isMan() { System.out.println("是个男人"); } }
-
public class Woman extends Person { boolean isBeautiful; //对父类中的eat()方法进行重写 public void eat() { System.out.println("女人饭量小"); } public void isWoman() { System.out.println("是个女人"); } }
-
public class PersonTest { public static void main(String[] args) { Person p = new Man(); p.eat(); //输出男人饭量大 p.sleep(); //输出睡觉 p.isMan(); //报错 }
2. 多态性不适用于属性
- 对于属性,编译和运行都看左边
-
public class Person { String name; int age; int id = 1; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } }
-
public class Man extends Person { boolean isStrong; int id = 2; //对父类中的eat()方法进行重写 public void eat() { System.out.println("男人饭量大"); } public void isMan() { System.out.println("是个男人"); } }
-
public class PersonTest { public static void main(String[] args) { Person p = new Man(); System.out.println(p.id); //输出1 }
3. 虚拟方法调用的再理解
- 子类中定义了与父类同名同参的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法,这样的方法调用在编译期是无法确定的,故多态是运行时行为
- 编译时是父类类型,运行时子类类型,即动态绑定
- 重载可以理解为“早绑定”或“静态绑定”,重写可以理解为“晚绑定”或“动态绑定”
4. 向下转型的使用
- 不能调用子类所特有的方法和属性
- 有了对象的多态性以后,内存中实际上是加载了子类特有的方法和属性的,但是由于变量声明为父类类型,导致编译时只能调用父类中声明的方法和属性,子类特有的方法和属性不能调用
- 如何才能调用子类特有的方法和属性
- 向下转型:使用强制类型转换符
-
public class PersonTest { public static void main(String[] args) { Person p = new Man(); System.out.println(p.id); //输出1 p.isMan(); //报错 p.isStrong = true; //报错 //向下转型:使用强制类型转换符 Man m = (Man)p; m.isMan(); //成功,输出是个男人 m.isStrong = true; //成功 //使用强转时,可能出现ClassCastException的异常 Woman w = (Woman)p; w.isWoman(); //报错 }
5. instanceof关键字的使用
- a instanceof A:判断对象a是否是类A的实例,如果是,返回true,如果不是,返回false
- 为了避免在向下转型时出现ClassCastException的异常,我们在向下转型之前,先进行instanceof的判断,如果返回true,则进行向下转型,如果返回false,则不进行
- 如果a instanceof A返回true,则a instanceof B也返回true,那么B是A的父类
-
public class PersonTest { public static void main(String[] args) { Person p = new Man(); System.out.println(p.id); //输出1 p.isMan(); //报错 p.isStrong = true; //报错 //向下转型:使用强制类型转换符 Man m1 = (Man)p; m1.isMan(); //成功,输出是个男人 m1.isStrong = true; //成功 // //使用强转时,可能出现ClassCastException的异常 // Woman w = (Woman)p; // w.isWoman(); //报错 if(p instanceof Woman) { Woman w = (Woman)p; w.isWoman(); //不报错,也不输出,因为返回值为false } if(p instanceof Man) { Man m2 = (Man)p; m2.isMan(); //输出是个男人,因为返回值为true } }
七、Object类的使用
1. Object类结构的剖析
- Object类是所有Java类的根父类
- 如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object类
- Object类中的功能 (属性和方法) 具有通用性
- 属性:无
- 方法:equals() / toString() / getClass() / hashCode() / clone() / finalize() / wait() / notify() / notifyAll()
- Object类只声明了一个空参的构造器
2. equals()的使用
- ==和equals()的区别
- ==是运算符,既可以比较基本数据类型,也可以比较引用数据类型,对于基本数据类型,是比较值 (不一定类型要相同),对于引用数据类型,是比较内存地址,即两个引用是否指向同一个对象实体
- equals()是方法,而非运算符,只适用于引用数据类型,属于java.lang.Object类的方法,如果该方法没有被重写,则默认也是==
- 我们可以看到String等类的equals()方法是被重写过的,而且String类在日常开发中用的较多,久而久之,形成了equals()方法是比较值的错误观点
- 具体要看自定义类中有没有重写Object类的equals()方法来判断
- 通常情况下,重写equals()方法,会比较类中的相应属性是否都相等
- 像String、Data、File、包装类等都重写了Object类中的equals()方法,重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同
-
public class Person { String name; int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } }
-
public class PersonTest { public static void main(String[] args) { Person p1 = new Person("Tom", 25); Person p2 = new Person("Tom", 25); String str1 = new String("CSDN"); String str2 = new String("CSDN"); System.out.println(p1 == p2); //输出false System.out.println(str1 == str2); //输出false System.out.println(p1.equals(p2)); //输出false System.out.println(str1.equals(str2)); //输出true }
- 我们自定义的类如果使用equals()的话,也通常是比较两个对象的“实体内容”是否相同,那么我们就需要对Object类中的equals()方法进行重写
- 重写的原则:比较两个对象的“实体内容” (本例子中是name和age) 是否相同
- IDEA可以自动生成,快捷键为ALT + INS,推荐使用自动生成
-
public class Person { String name; int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } public boolean equals(Object obj) { if(this == obj) { return true; } if(obj instanceof Person) { Person person = (Person)obj; //比较两个对象的每个属性是否都相同 // if(this.name.equals(person.name) && this.age == person.age) { // return true; // }else { // return false; // } //或者 return this.name.equals(person.name) && this.age == person.age; } return false; } }
-
public class PersonTest { public static void main(String[] args) { Person p1 = new Person("Tom", 25); Person p2 = new Person("Tom", 25); System.out.println(p1.equals(p2)); //输出true }
3. toString()的使用
- 当我们输出一个对象的引用时,实际上就是调用当前对象的toString()
- 像String、Data、File、包装类等都重写了Object类中的toString()方法,使得在调用对象的toString()时,返回“实体内容”信息
- 自定义类也可以重写toString()方法,当调用此方法时,返回对象的“实体内容”
- IDEA可以自动生成,快捷键为ALT + INS,推荐使用自动生成
八、包装类 (Wrapper) 的使用
1. 包装类的理解
- 针对8种基本数据类型定义相应的引用数据类型,即包装类 (封装类)
- 有了类的特点,就可以调用类中的方法,Java才是真正的面向对象
-
基本数据类型 包装类 说明 byte Byte 包装类的父类:Number short Short int Integer long Long float Float double Double boolean Boolean char Character
2. 基本数据类型转换为包装类
-
public class WrapperTest { public static void main(String[] args) { //基本数据类型 ---> 包装类:调用包装类的构造器 int num = 10; System.out.println(num.toString()); //报异常 Integer int1 = new Integer(num); System.out.println(int1.toString()); //输出10 Integer int2 = new Integer("123"); System.out.println(int2.toString()); //输出123 Integer int3 = new Integer("123abc"); System.out.println(int3.toString()); //报异常 }
3. 包装类转换为基本数据类型
-
public class WrapperTest { public static void main(String[] args) { //包装类 ---> 基本数据类型:调用包装类Xxx的xxxValue() Integer int1 = new Integer(26); int int2 = int1.intValue(); System.out.println(int2 + 1); //输出27 }
4. 自动装箱与自动拆箱
-
public class WrapperTest { public static void main(String[] args) { //自动装箱:基本数据类型 ---> 包装类 int int1 = 10; Integer int2 = num; //自动装箱 System.out.println(int2.toString()); //输出10 boolean b1 = true; Boolean b2 = b1; //自动装箱 //自动拆箱:包装类 ---> 基本数据类型 Integer int3 = new Integer(26); int int4 = int3; //自动拆箱 }
5. 基本数据类型包装类与String的相互转换
-
public class WrapperTest { public static void main(String[] args) { //基本数据类型、包装类 ---> String类型:调用String重载的valueOf(Xxx xxx) //方式1:连接运算 int int1 = 10; String str1 = num + ""; //方式2:调用String重载的valueOf(Xxx xxx) float f1 = 12.3f; String str2 = String.valueOf(f1); Double d1 = new Double(45.6); String str3 = String.valueOf(d1); }
-
public class WrapperTest { public static void main(String[] args) { //String类型 ---> 基本数据类型、包装类:调用包装类的parseXxx(String s) String str1 = "123"; int num = Integer.parseInt(str1); System.out.println(num + 1); //输出124 }
6. 包装类的常见面试题
-
public class WrapperTest { public static void main(String[] args) { Object obj = true ? new Integer(1) : new Double(2.0); System.out.println(obj); //输出1.0 }
-
public class WrapperTest { public static void main(String[] args) { Integer i = new Integer(1); Integer j = new Integer(1); System.out.println(i == j); //输出false //Integer内部定义了IntegerCache结构,IntegerCache中定义了Integer[] //Integer[]保存了从-128~127范围的整数 //如果我们使用自动装箱的方式给Integer赋值的范围在-128~127范围内时 //可以直接使用数组中的元素,不用再去new了 //目的:提高效率 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 }