文章目录
本文章由公号【开发小鸽】发布!欢迎关注!!!
老规矩–妹妹镇楼:
一、面向对象特征之封装
(一)概念
封装,顾名思义将类中的属性以及功能进行封装。由于对象内部的复杂性,为了便于外界的调用,只需要对外部公开简单的调用接口,而将需要隐藏的数据进行隐藏,可见封装性既能够保证数据的隐私性,防止调用者对类内部定义的属性操作导致数据的错误以及安全性问题,也能够保证外界对于该类中接口的调用。
(二)封装方式
通过声明类中的属性变量为private以隐藏数据,防止外部调用者直接通过类对象调用得到指定的数据信息,并通过提供public的getXXX()和setXXX()方法获取指定的数据以及设置数据变量的数值。使用这种方式能够提高类中属性变量的安全性。
class Animal {
private int legs;// 将属性legs定义为private,只能被Animal类内部访问
public void setLegs(int i) { // 在这里定义方法 eat() 和 move()
if (i != 0 && i != 2 && i != 4) {
System.out.println("Wrong number of legs!");
return;
}
legs = i;
}
public int getLegs() {
return legs;
}
}
还可以通过设置private的方法,来防止外部的调用,最常见的使用方法是单例模式中的私有构造方法,禁止外部直接调用该构造方法,构造第二个对象。
(三)权限修饰符
权限修饰符通常定义在类、成员属性、成员方法之前,用来限定类对象对于该类中成员的访问权限。对于类的权限修饰只可以使用public和default,而对于类中的成员则可以使用多种权限修饰符,我们在记忆多种权限修饰符的时候可以采用阶梯式的方式记忆,如下所示:
可以从图中看到,从private、default、protected到public,类中的权限呈现阶梯形式地增长,从同一个类内部、同一个包内部、不同包的子类中以及同一个工程内。
二、面向对象特征之继承
(一)概念
面向对象的第二个特征是继承,当我们需要构造多个类且多个类中存在一些相同的属性和方法时,可以将相同的内容抽取到一个单独的类中,并通过继承的方式构造多个类来调用这些相同的属性和方法。该单独的类称为父类或基类,使用继承方式创建的多个类称为子类或派生类。
(二)使用方式
类的继承方式如下所示,通过extends来继承父类:
class SubClass extends SuperClass{}
extends为扩展的意思,也就是说子类既可以使用父类中的部分属性和方法,也可以自定义属性和方法,即扩展了父类的属性和方法,减少了代码的冗余,提高了代码的复用性。需要注意的是,由于封装性,子类是不能直接访问父类中的private属性和方法的,但也是可以通过get或set方法访问到private的父类属性。
而且,Java中只支持类的单继承和多层继承,不支持类的多继承,也就是一个类只允许extends单个父类,不能同时extends多个父类:
class SubDemo extends Demo{ } //ok
class SubDemo extends Demo1,Demo2...//error
所谓的多层继承就是可以一层一层地进行继承,即一个子类有直接的父类,该父类也会有直接的父类,因此子类就会有间接的父类,类的继承就是这样一层层地延伸,所有类都会默认有一个顶级的父类:java.lang.Object。因此,如果一个类没有通过extends继承父类,则它会默认继承java.lang.Object类。
需要注意的是,子类可以直接获取父类以及间接父类的部分属性与方法!
(三)继承的优点
类的继承特性有如下的优势:
-
子类可直接调用父类中的方法,无需在子类中重复编写代码,减少了代码的冗余,提高了代码的复用性;
-
当需要扩展一个类的功能时,不需要修改原有类的属性和方法,而是通过extends继承原有类,然后在子类中新添加属性和方法,功能的扩展非常方便;
(四)方法的重写
1.定义
所谓的方法重写指的是,在子类中可以根据需要对从父类中继承来的方法进行重新改造,也就是重新写继承的方法,该方法在程序执行时将会覆盖父类的方法。
2.重写方式
(1)子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表;
(2)子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型,通俗点来说就是必须是父类方法的返回值或者是它的子类,如果是基本数据类型则必须一致;
(3)子类重写的方法使用的访问权限不能小于父类被重写方法的访问权限,如父类方法的权限为public,而子类方法的权限为private ,则该方法的重写是非法,理解为权限小的子类方法不能访问权限较大的父类的被重写方法。但要注意的是,子类不能重写父类中声明为private的方法。
(4)子类方法抛出的异常不能大于父类被重写方法的异常,也就是说子类方法抛出的异常必须是父类被重写方法的异常或异常的子类。这是因为如果子类方法抛出的异常过大,则父类被重写方法是无法catch到的,会出现异常;
(5)只有非static方法才能够重写,static方法是不能重写的,因为static方法是属于类的,随着类的加载而加载;
-
public class Person { public String name; public int age; public String getInfo() { return "Name: "+ name + "\n" +"age: "+ age; } } public class Student extends Person { public String school; public String getInfo() { //重写方法 return "Name: "+ name + "\nage: "+ age + "\nschool: "+ school; } public static void main(String args[]){ Student s1=new Student(); s1.name="Bob"; s1.age=20; s1.school="school2"; System.out.println(s1.getInfo()); //Name:Bob age:20 school:school2 } }
(五)super关键字
之前我们介绍了this关键字,代表当前类对象的引用,super关键字与this类似,代表父类的内存空间的标识,也就是说能够调用父类中的属性和方法。super关键字通常用来进行如下的操作:
- 访问父类中定义的属性,尤其是当子父类中出现同名的成员时,可以使用super区分出父类中的成员;
- 调用父类中定义的成员方法;
需要注意的是,super对于所谓父类的调用追溯不仅仅局限于直接父类,还可以一直追溯到间接父类以及一层层之上的父类。
虽然说super关键字较少用到,但是在子类的构造器中其实都会默认访问父类中的空参构造器,也就是调用super(),也就是说子类中的每个构造器中即使没有显式地写出super(),其实都默认调用了父类的空参构造器。如果父类中没有空参构造器,子类的构造器必须通过this(参数列表)或super(参数列表)语句来调用父类中的构造器,否则会编译出错。
public class Person {
private String name;
private int age;
private Date birthDate;
public Person(String name, int age, Date d) {
this.name = name;
this.age = age;
this.birthDate = d;
}
public Person(String name, int age) {
this(name, age, null);
}
public Person(String name, Date d) {
this(name, 30, d);
}
public Person(String name) {
this(name, 30);
}
}
上面为Person父类,下面为Student子类,可以看到Person类中没有空参构造函数,因此Student子类中无法默认调用Person类的空参构造方法,只能通过super调用Person类的有参构造函数,如果没有显式地调用Person类中的有参构造函数,则会出现编译错误。
public class Student extends Person {
private String school;
public Student(String name, int age, String s) {
super(name, age);
school = s;
}
public Student(String name, String s) {
super(name);
school = s;
}
// 编译出错: no super(),系统将调用父类无参数的构造器。
public Student(String s) {
school = s;
}
}
(六)子类对象的实例化过程
一个子类对象的实例化过程是通过父子类的继承关系来实现的,不管是调用自己的构造器方法还是父类的构造器方法,最终都会一层层地调用父类的构造器方法,直到最后的Object类的空参构造器中。这样,就会将所有父类的属性和方法加载到子类对象的内存中,但是并没有创建父类对象。
可以看到的是,类对象的成员变量默认初始化总是在最初完成,接着是构造方法中的形参复制,并对this以及super关键字调用的当前类以及父类的构造器进行初始化,最后对当前类中的成员变量显式初始化,并执行构造方法中的代码。
三、面向对象特征之多态
(一)定义
面向对象的第三个特征是多态,所谓的多态的定义是:父类的引用指向子类的对象,这个定义比较难以理解。在前文中我们提到继承的概念,子类通过extends关键字继承父类的属性和方法,因此子类可以看做是特殊的父类,将父类类型的引用指向子类的对象,实现多种状态的实时绑定。
多态性的出现是因为编译时期与运行时期的对象类型不一致,Java引用变量分为编译时类型和运行时类型,编译时类型由该变量声明时使用的类型决定,运行时类型由实际赋予该变量的对象决定。
(二)多态的特点
多态是在继承的基础上发展而来的,一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,则该变量就不能访问子类中特有的属性和方法。
对于类中的属性变量而言是没有多态性的,声明什么类型就是什么类型,而类中的方法就不同了,在方法调用时需根据引用对象实际赋予的对象类型决定,这也就是多态的意义,可实时调用子类中不同的重写方法。
(三)虚方法
所谓的虚方法指的是,在子类中重写了父类中的方法,此时父类的方法称为虚方法。因此在多态定义下,当父类的引用指向了不同的子类对象时,可以动态调用属于该子类的重写方法,具体调用什么方法在编译器是无法确定的,只有在运行时才会动态绑定。
(四)instanceof操作符
在源码中我们常常会发现instanceof操作符的踪影,该操作符也与多态息息相关,通常用于强制类型转换前的安全检查,如 x instanceof A,检验x是否是类A或A的子类,如果是则返回ture。
那么,为什么要这样检查呢?我们知道对于基本数据类型,如果从小的数据类型转换为更大的数据类型是可以自动转换的,如int转换为long,但是如果反过来可能就会出现精度丢失的问题。对于类对象的转换也需要进行提前校验,在强制类型转换时,直接将该对象的类型A转换为类型B,此时是不会报异常的,因为它所做的操作只是将内存地址进行了修改,内存地址由两部分组成,前一部分是类型,后一部分是地址。强转之后,当我们调用强转后的类型中特有的方法或属性时,就会出现异常。因此使用instanceof来检查类型,避免在调用方法或属性时出现调用的异常。
若x是类A的子类,x instanceof A 返回的是true,则说明x可以强转为类A,这是因为x中加载类父类A的所有属性和方法,即使强转为父类A,调用父类A的属性和方法时可以找到对应的内存地址,不会出现调用的异常问题。
使用场景:在类对象强制转换之前,首先使用instanceof判断是否可以进行转换,如果返回true,则进行强转。
如String类型的equals()方法中即使用了instanceof:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
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;
}
(五)对象类型转换
1.基本数据类型的转换
小的数据类型可以自动转换为大的数据类型,如int转换为long,float转换为double;
大的数据类型可以强制转换为小的数据类型,但这种情况下可能会存在精度的丢失;
2.对象类型的转换
对于具有继承关系的两个类,从子类到父类的类型转换可以自动进行;
从父类到子类的类型转换必须通过强制类型转换实现,没有继承关系的引用类型之间的转换是非法的,因此在转换之前可以使用instanceof操作符测试一下对象的类型;
关于是否可以强制类型转换,在通过new构建该对象时右侧使用的类型A是否加载了后续需要进行强制类型转换类型B的属性和方法,即A是否是B以及B的子类,如果是则可以进行类型转换。
public class Test {
public void method(Person e) { // 设Person类中没有getschool() 方法
// System.out.pritnln(e.getschool()); //非法,编译时错误
if (e instanceof Student) {
Student me = (Student) e; // 将e强制转换为Student类型
System.out.pritnln(me.getschool());
}
}
public static void main(String[] args){
Test t = new Test();
Student m = new Student();
t.method(m);
}
}