在软件开发领域,面向对象编程(Object-Oriented Programming)是一种强大且广泛应用的编程范式,它将数据和操作封装在对象中,通过对象之间的交互来实现程序的逻辑。面向对象编程让我们能够以更加模块化、灵活和可维护的方式构建软件系统。
本文将链接上一篇文章,继续深入探讨面向对象编程的核心概念、优势以及如何在实际项目中应用这种编程范式
目录
6.1 外部类要跨包使用必须是public,否则仅限于本包使用
8.4 虚方法调用(Virtual Method Invocation)
权限修饰符:public、protected、缺省\private
| 修饰符 | 本类 | 本包 | 其他包子类 | 其他包非子类 |
| private | √ | × | × | × |
| 缺省 | √ | √(本包子类非子类都可见) | × | × |
| protected | √ | √(本包子类非子类都可见) | √(其他包限于子类中可见) | × |
| public | √ | √ | √ | √ |
外部类:public和缺省
成员变量、成员方法等:public、protected、缺省、private
(1)外部类的权限修饰符如果缺省,本包使用没问题
(2)外部类的权限修饰符如果缺省,跨包使用有问题
(1)本包下使用:成员的权限修饰符可以是public、protected、缺省
(2)跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义
在Java类中使用super来调用父类中的指定操作:
1、super可用于访问父类中定义的属性
2、super可用于调用父类中定义的成员方法
3、super可用于在子类构造器中调用父类的构造器
注意:
1、尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
2、super的追溯不仅限于直接父类
3、super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则默认调用的子类重写的方法
示例:
- public class Phone {
- public void sendMessage(){
- System.out.println("发短信");
- }
- public void call(){
- System.out.println("打电话");
- }
- public void showNum(){
- System.out.println("来电显示号码");
- }
- }
-
- //smartphone:智能手机
- public class SmartPhone extends Phone{
- //重写父类的来电显示功能的方法
- public void showNum(){
- //来电显示姓名和图片功能
- System.out.println("显示来电姓名");
- System.out.println("显示头像");
-
- //保留父类来电显示号码的功能
- super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
- }
- }
方法前没有super.和this.
先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
方法前有this.
先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
方法前有super.
从当前子类的直接父类找,如果没有,继续往上追溯
如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问
示例:
- class Father{
- int a = 10;
- int b = 11;
- }
- class Son extends Father{
- int a = 20;
-
- public void test(){
- //子类与父类的属性同名,子类对象中就有两个a
- System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
- System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
- System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
-
- //子类与父类的属性不同名,是同一个b
- System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
- System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
- System.out.println("b = " + super.b);//11 直接从父类局部变量找
- }
-
- public void method(int a, int b){
- //子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
- System.out.println("局部变量的a:" + a);//30 先找局部变量
- System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
- System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
-
- System.out.println("b = " + b);//13 先找局部变量
- System.out.println("b = " + this.b);//11 先从本类成员变量找
- System.out.println("b = " + super.b);//11 直接从父类局部变量找
- }
- }
- class Test{
- public static void main(String[] args){
- Son son = new Son();
- son.test();
- son.method(30,13);
- }
- }
变量前面没有super.和this.
在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的局部变量,
如果不是局部变量,先从当前执行代码的本类去找成员变量
如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(权限修饰符允许在子类中访问的)
变量前面有this.
通过this找成员变量时,先从当前执行代码的==本类去找成员变量==
如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量(==权限修饰符允许在子类中访问的)
变量前面有super.
通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)
1、子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器。
2、规定:“super(形参列表)”,必须声明在构造器的首行。
3、我们前面讲过,在构造器的首行可以使用"this(形参列表)",调用本类中重载的构造器,在构造器的首行,"this(形参列表)" 和 "super(形参列表)"只能二选一。
4、如果在子类构造器的首行既没有显示调用"this(形参列表)",也没有显式调用"super(形参列表)", 则子类此构造器默认调用"super()",即调用父类中空参的构造器。
5、子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。 只能是这两种情况之一。
6、一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)",则剩下的那个一定使用"super(形参列表)"。
开发中常见错误
如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则
编译出错。
this:当前对象
在构造器和非静态代码块中,表示正在new的对象
在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
this:
1、this.成员变量:表示当前对象的某个成员变量,而不是局部变量
2、this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
3、this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
super:
1、super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
2、super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
3、super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错
多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象
格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)
父类类型 变量名 = 子类对象
示例:
- Person p = new Student();
-
- Object o = new Person();//Object类型的变量o,指向Person类型的对象
-
- o = new Student(); //Object类型的变量o,指向Student类型的对象
对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象
Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译时,看左边;运行时,看右边。
多态的使用前提:1、类的继承关系 2、方法的重写
示例:
- public class Pet {
- private String nickname; //昵称
-
- public String getNickname() {
- return nickname;
- }
-
- public void setNickname(String nickname) {
- this.nickname = nickname;
- }
-
- public void eat(){
- System.out.println(nickname + "吃东西");
- }
- }
- public class Cat extends Pet {
- //子类重写父类的方法
- @Override
- public void eat() {
- System.out.println("猫咪" + getNickname() + "吃鱼仔");
- }
-
- //子类扩展的方法
- public void catchMouse() {
- System.out.println("抓老鼠");
- }
- }
- public class Dog extends Pet {
- //子类重写父类的方法
- @Override
- public void eat() {
- System.out.println("狗子" + getNickname() + "啃骨头");
- }
-
- //子类扩展的方法
- public void watchHouse() {
- System.out.println("看家");
- }
- }
1、方法内局部变量的赋值体现多态
- public class TestPet {
- public static void main(String[] args) {
- //多态引用
- Pet pet = new Dog();
- pet.setNickname("小白");
-
- //多态的表现形式
- /*
- 编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
- 运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
- */
- pet.eat();//运行时执行子类Dog重写的方法
- // pet.watchHouse();//不能调用Dog子类扩展的方法
-
- pet = new Cat();
- pet.setNickname("雪球");
- pet.eat();//运行时执行子类Cat重写的方法
- }
- }
2、方法的形参声明体现多态
- public class Person{
- private Pet pet;
- public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
- this.pet = pet;
- }
- public void feed(){
- pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
- }
- }
- public class TestPerson {
- public static void main(String[] args) {
- Person person = new Person();
-
- Dog dog = new Dog();
- dog.setNickname("小白");
- person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
- person.feed();
-
- Cat cat = new Cat();
- cat.setNickname("雪球");
- person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
- person.feed();
- }
- }
3、方法返回值类型体现多态
- public class PetShop {
- //返回值类型是父类类型,实际返回的是子类对象
- public Pet sale(String type){
- switch (type){
- case "Dog":
- return new Dog();
- case "Cat":
- return new Cat();
- }
- return null;
- }
- }
- public class TestPetShop {
- public static void main(String[] args) {
- PetShop shop = new PetShop();
-
- Pet dog = shop.sale("Dog");
- dog.setNickname("小白");
- dog.eat();
-
- Pet cat = shop.sale("Cat");
- cat.setNickname("雪球");
- cat.eat();
- }
- }
开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
示例:
(1)声明一个Dog类,包含public void eat()方法,输出“狗啃骨头”
- public class Dog {
- public void eat(){
- System.out.println("狗啃骨头");
- }
- }
(2)声明一个Cat类,包含public void eat()方法,输出“猫吃鱼仔”
- public class Cat {
- public void eat(){
- System.out.println("猫吃鱼仔");
- }
- }
(3)声明一个Person类, 包含宠物属性,包含领养宠物方法 public void adopt(宠物类型Pet),包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
- public class Person {
- private Dog dog;
-
- //adopt:领养
- public void adopt(Dog dog){
- this.dog = dog;
- }
-
- //feed:喂食
- public void feed(){
- if(dog != null){
- dog.eat();
- }
- }
- }
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
- Person e = new Student();
- e.getInfo(); //调用Student类的getInfo()方法
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量
- public class TestVariable {
- public static void main(String[] args) {
- Base b = new Sub();
- System.out.println(b.a);
- System.out.println(((Sub)b).a);
-
- Sub s = new Sub();
- System.out.println(s.a);
- System.out.println(((Base)s).a);
- }
- }
- class Base{
- int a = 1;
- }
- class Sub extends Base{
- int a = 2;
- }
首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过。
向上转型:自动完成
向下转型:(子类类型)父类变量
- public class ClassCastTest {
- public static void main(String[] args) {
- //没有类型转换
- Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
-
- //向上转型
- Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
- pet.setNickname("小白");
- pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
- // pet.watchHouse();//不能调用父类没有的方法watchHouse
-
- Dog d = (Dog) pet;
- System.out.println("d.nickname = " + d.getNickname());
- d.eat();//可以调用eat方法
- d.watchHouse();//可以调用子类扩展的方法watchHouse
-
- Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
- //这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
- }
- }
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。如下代码格式:
- //检验对象a是否是数据类型A的对象,返回值为boolean型
- 对象a instanceof 数据类型A
说明:
1、只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
2、如果对象a属于类A的子类B,a instanceof A值也为true。
3、要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
代码:
- public class TestInstanceof {
- public static void main(String[] args) {
- Pet[] pets = new Pet[2];
- pets[0] = new Dog();//多态引用
- pets[0].setNickname("小白");
- pets[1] = new Cat();//多态引用
- pets[1].setNickname("雪球");
-
- for (int i = 0; i < pets.length; i++) {
- pets[i].eat();
-
- if(pets[i] instanceof Dog){
- Dog dog = (Dog) pets[i];
- dog.watchHouse();
- }else if(pets[i] instanceof Cat){
- Cat cat = (Cat) pets[i];
- cat.catchMouse();
- }
- }
- }
- }
类 java.lang.Object是类层次结构的根类,即所有其它类的父类。每个类都使用 Object 作为超类。
Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用
- method(Object obj){…} //可以接收任何类作为其参数
-
- Person o = new Person();
- method(o);
所有对象(包括数组)都实现这个类的方法。
如果一个类没有特别指定父类,那么默认则继承自Object类。例如:
- public class Person {
- ...
- }
- //等价于:
- public class Person extends Object {
- ...
- }
equals():所有类都继承了Object,也就获得了equals()方法。还可以重写。
只能比较引用类型,Object类源码中equals()的作用与“==”相同:比较是否指向同一个对象。
当自定义使用equals()时,可以重写。用于比较两个对象的“内容”是否都相等
重写equals()方法的原则
1、对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。2、自反性:x.equals(x)必须返回是“true”。3、传递性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。4、一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。
5、任何情况下,x.equals(null),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false
方法签名:public String toString()
1、默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"
2、在进行String与其它类型数据的连接操作时,自动调用toString()方法
- Date now=new Date();
- System.out.println(“now=”+now); //相当于
- System.out.println(“now=”+now.toString());
3、 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()
4、 可以根据需要在用户自定义类型中重写toString()方法 如String 类重写了toString()方法,返回字符串的值。
- s1="hello";
- System.out.println(s1);//相当于
- System.out.println(s1.toString());
当对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的)
永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
什么时候被回收:当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
在JDK 9中此方法已经被标记为过时的。
- public class FinalizeTest {
- public static void main(String[] args) {
- Person p = new Person("Peter", 12);
- System.out.println(p);
- p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
- System.gc();//强制性释放空间
- }
- }
-
- class Person{
- private String name;
- private int age;
-
- public Person(String name, int age) {
- super();
- this.name = name;
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- //子类重写此方法,可在释放对象前进行某些操作
- @Override
- protected void finalize() throws Throwable {
- System.out.println("对象被释放--->" + this);
- }
- @Override
- public String toString() {
- return "Person [name=" + name + ", age=" + age + "]";
- }
-
- }
public final Class> getClass():获取对象的运行时类型
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法
- public static void main(String[] args) {
- Object obj = new Person();
- System.out.println(obj.getClass());//运行时类型
- }
public int hashCode():返回每个对象的hash值。
- public static void main(String[] args) {
- System.out.println("AA".hashCode());//2080
- System.out.println("BB".hashCode());//2112
- }