封装
- 什么是封装?
- 封装是指将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供对外简单的操作入口(方法)来对隐藏的信息进行操作和访问。
- 封装之后才形成了真正的“对象”,真正的“独立体”。
- 封装就意味着以后的程序可以重复使用,并且这个事物适应性比较强,在任何场合都可以使用。
- 封装之后,对于事物本身,提高了安全性。
- 封装的优点:
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员变量进行更精确的控制。
- 隐藏信息,实现细节。
- 提高代码的安全性。
- 提高代码的复用性。
- “高内聚”:封装细节,便于修改内部代码,提高可维护性。
- “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作
- java 封装,说白了就是将一大坨玩意,装到一个盒子里(class),出入口都在这个盒子上。你要用就将这个盒子拿来用,连接出入口,就能用了,不用就可以直接扔,对你代码没什么影响。
- 实现封装的步骤
- 所有属性私有化,使用private关键字进行修饰,(private表示私有的修饰的属性只能在本类中访问)。
- 对外提供简单的操作入口:
- 对外提供两种公开的方法:set方法和get方法,通过这两种方法对数据进行获取和设定,对象通过调用这两种方法实现对数据的读写。
- set方法命名规范:set+属性名首字母大写
- get方法命名规范:get+属性名首字母大写
- 在getter/setter方法中加入属性控制语句(对属性值的合法性进行判断)
- setter and getter方法都没有static关键字【因为setter和getter方法一般都是对当前对象的实例变量进行操作的,如果带有static关键字在方法中就不可以使用this关键字,肯定也操作不到当前对象的实例变量,失去了getter和setter方法存在的价值】
- 有static关键字修饰的方法怎么调用:类名.方法名(实参)
- 没有static关键字修饰的方法怎么调用:引用.方法名(实参)
继承
- 子类继承父类除了构造方法之外的所有东西,只不过因为private修饰的属性因作用域的问题在子类中无法直接访问父类的私有属性(因为继承是发生在对象级别的,所以所有子类对象【包括所有子类对象的引用】,也都无法访问父类对象的私有属性),私有属性是通过在父类类体中“父类对象引用.父类私有属性”访问的,其他的属性都可以直接通过“子类对象引用.属性名”的方式直接访问。
- 一个类中的私有属性,只能在本类中由本类的对象的引用去访问。
- 类加载的时候,有继承关系的父子类,JVM先加载父类再去加载子类。
- 在本类方法当中this,一定是本类的对象的引用。
- 子类肯定是他爹类型的。
- 继承基本的作用是:代码复用。但是继承最重要的作用是:有了继承才有了以后的“方法覆盖”和“多态”。
- 继承语法格式:
- [修饰符列表] class 类名 extends 父类名 {
类体 = 属性 + 方法;
}
- Java中继承只支持单继承,一个类不能同时继承很多类,只能继承一个类。【在c++中支持多继承】。
- 关于继承中的一些术语:
- B类继承A类,其中:
- A类称为:父类、超类、基类、superclass
- B类称为:子类、派生类、扩展类、subclass
- B类继承A类,其中:
- 在Java语言中子类继承的父类的哪些数据?
- 构造方法不支持继承
- 其他数据都可以被继承
- Java中虽然只支持单继承,但是一个类也可以间接继承其他类
- C extends B {}
- B extends A {}
- A extends T {}
- C类直接继承B类,但是C类间接继承A、T类。
- Java语言中假设一个类没有显示继承任何类,该类默认继承JavaSE类库中提供的java.lang.Object类。【证明所有的类都直接或者间接继承java.lang.Object类,Java语言中任何一个类都有Object类的特征。】
public class User {
private String name;
public String getName() {
//实验:因为private属性只能在本类中访问,在父类中将父类对象转换成子类对象再去调用父类的私有name属性,也会编译报错,因为子类引用直接点出来父类的私有属性不就相当于直接访问了吗。因为子类对象不可见父类的私有属性,虽然再父类类体中,但也会报错。【private修饰的属性只能再本类中由本类的对象的引用访问,如果想再外面访问可以提供一个公开的方法接口,在方法中加上限制,间接调用本类的私有特征】
//return ((Student)this).name; 在子类调用通过父类继承过来的这个方法的时候这个方法的时候其中的this是子类的引用,但是子类引用又不能直接调用父类的私有属性,所以这里的this会将子类当前对象引用自动转换成父类当前对象引用【因为孩子肯定是他爹类型的 ,可以进行自动类型转换】
return this.name;
}
public void setName(String name) {
User.this.name = name;//这里只能写本类的当前对象“User.this.”,即使其他类中有name属性,也不能写其他类的当前对象,“其它类名.this.name = name”这么些会报错“不可访问其他类型的封闭实例”。
}
}
- 成员变量的隐藏:“在父子类中,是可以出现同名的成员变量的,那么如果创建子类对象,然后访问该同名成员变量,访问到的结果是子类中该成员变量的取值。父子类成员变量存储在子类对象中的不同区域,由于某种访问机制,属性被隐藏了,这种现象称之为”属性的隐藏。
- 不管是父类私有的成员也好公开的成员也好,实例化子类对象的时候虽然子类继承了父类除构造方法之外的东西,但是也不是父子类各有一份存储空间,而是只有一份存储空间父子类访问相同的存储空间。 【这里也好理解,比如说他爹有100万财产(这里的财产代表父类的成员),想有孩子肯定是先有他爹,孩子继承了他爹的财产,在实例化子类对象的时候,先创建他爹对象,他爹把这100万财产的内存空间给开辟出来了,并且只有这一份内存空间,不能说孩子和他爹各有100万财产,那不赚大了吗,100万秒变200万,而是孩子可以拿他爹的100万来花,这100万还得是他爹的100万存储空间,但是他爹要是有私有的属性(他爹的私人空间),(他爹如果有不想让孩子知道的事情(private属性),那么孩子就没有办法直接访问,孩子是需要通过他爹提供的方法才能间接访问),只能通过他爹提供公开的接口(set和get)方法去访问】
- 子类继承父类所有的属性和方法,非私有的属性和方法可以在子类中直接访问,但是私有的属性和方法不能再子类中直接访问,但可以通过公共的方法去访问,【因为私有的属性和方法只能在本类中由本类对象的引用去访问】。
- 不能滥用继承,子类和父类之间必须满足is-a的逻辑关系。
- 继承的本质
- 当子类对象创建好后,父子类建立的是一种查找关系。
- 一下程序的内存图
public class Test { //描述查找关系
public static void main(String[] args) {
Son s = new Son();
//按照什么关系来返回信息呐?
//(1)实例化一个子类对象,用子类对象去调用属性或方法,首先看子类是否有属性或方法
//(2)如果子类中有这个属性或方法,并且在调用处可以访问,则返回信息
//(3)如果子类中没有这个属性或方法,就查找父类中有没有这个属性或方法(如果父类中有该属性或方法,并且在调用处可以访问,则返回信息)
//(4)如果父类中再没有就按照(3)的规则,继续找上级父类,直到Object.....
System.out.println(s.name);
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa {
String name = "大头爸爸";
int age = 39;//如果该属性是私有的在子类实例化对象的时候在堆内存的内存空间也是开辟的,只不过子类中无法直接访问。
}
class Son extends Father {
String name = "大头儿子";
}
public class Test { //描述查找关系
public static void main(String[] args) {
Son s = new Son();
//按照什么关系来返回信息呐?
//(1)实例化一个子类对象,用子类对象去调用属性或方法,首先看子类是否有该属性或方法
//(2)如果子类中有这个属性或方法,并且在调用处可以访问,则返回信息
//(3)如果子类中没有这个属性或方法,就查找父类中有没有这个属性或方法(如果父类中有该属性或方法,并且在调用处可以访问,则返回信息)
//(4)如果父类中再没有就按照(3)的规则,继续找上级父类,直到Object.....
System.out.println(s.name);
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa {
String name = "大头爸爸";
int age = 39;
}
class Son extends Father {
String name = "大头儿子";
}
public class Test { //描述查找到哪里结束,就不再接着往上查找了
public static void main(String[] args) {
Son s = new Son();
//(1)实例化一个子类对象,用子类对象去调用属性或方法的时候,首先看子类是否有该属性或方法
//(2)如果子类中有这个属性或方法,并且在调用处可以访问,则返回信息
//(3)如果子类中没有这个属性或方法,就查找父类中有没有这个属性或方法(如果父类中有该属性或方法,但是在调用处不可见,会编译报错)
//(4)只要找到这个属性或方法的存在就不再往上找了,不管在调用处可见不可见都不再往上查找了。(如果在调用处可见,返回信息,如果不可见,编译报错。)哪怕父类的父类里面再有这个属性并且在调用处可见也不会往上找了。
//(5)如果父类中再没有就按照(4)的规则,继续找上级父类,直到Object.....
System.out.println(s.name);//比如此例:Son类中没有name属性,去父类中查找,父类中有name属性但是在此处不可见,所以编译报错。(即使父类的父类中再有name属性,并且再此处也可以访问,结果还是编译报错)。
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa {
private String name = "大头爸爸";
int age = 39;
}
class Son extends Father {}
public class Test {
public static void main(String[] args) {
Son s = new Son();
//如果通过子类对象的引用调用方法,从当前类开始向父类的方向寻找,在哪个父类找到该方法的时候,在方法执行过程中,子类对象已经隐式类型转换成哪个父类对象的引用。
System.out.println(s.getName());//比如此例:首先在Son类中寻找getName方法,没有找到。然后去 Father 类寻找,找到了并且在此处可以调用,在执行的过程中getName方法中的this是Father类的当前对象【因为在找到getName方法的时候,子类对象已经上转型Father类对象】,然后执行“return this.name;”,在此类中寻找name属性,没有找到,然后去Father类的父类GrandPa类中寻找name属性,不会从Son类中寻找。
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa {
int age = 39;
public String getName() {
return this.name;
}
}
class Son extends Father {
String name = "大头儿子";
}
为什么有些父类属性要定义为私有的?子类都继承了父类的私有成员,有但不能直接访问为什么这么设置?
比如说Animal,代表“动物类”,声明了一个私有的属性name,代表动物的“名字”,为该属性定义了get和set方法。又定义了一个Person类,代表“人类”,Person类继承了Animal类,就代表在人类中的每一个人(对象)姓名是不能随意更改的,只能通过到当地警局备案,审核通过后才可以更改,而get和set方法就代表了当地警局审核的过程。所以要将Animal类中的name属性定义为私有的,这样每一个子类的该属性得到了有效的控制,既有该属性的存储空间又不能直接访问。【为什么要在父类中定义控制该属性的私有化,而不再子类中控制,提高代码的复用性(也就是说只要他是一个动物他的名字就不能随意更改)。也可能是父类和子类不是同一个人编写的,父类的私有属性业务需求子类并不知道,子类直接去修改了父类的属性,就导致父类完犊子,所以父类声明该属性为私有的,子类只能通过set和get接口访问】。
多态
- 引出多态:比如此例描述主人给动物喂食物,主人第一天养了一只猫咪,猫咪喜欢吃鱼,用Java语言描述,就需要有一个主人类,有一个猫类有一个鱼类,在主人类中有一个主人给猫喂鱼的方法【该方法有两个参数,第一个参数是宠物的类型(在此就是猫类),第二个参数是食物的类型(在此就是鱼类)】。主人第二天又养了一只狗子,狗子喜欢吃骨头,用Java语言描述,就需要有一个狗子类和一个骨头类,再主人类里面再加一个给狗喂骨头的方法【该方法有两个参数,第一个参数是宠物的类型(在此就是狗类),第二个参数是食物的类型(在此就是骨头类)】。第三天主人又养了一只猪......,每次添加一只新宠物就要在主人类中添加一个新方法,这个方法都是主人喂宠物,功能相似但不是通用的,所以代码的复用度不高,且不利于维护。我们需要的是来一个动物,主人类代码不用动,还可以接着用。所以就要引出多态的概念。
- 父类型的引用指向子类型的对象这种机制导致程序在编译阶段和运行阶段绑定两种不同的状态/形态,这种机制称为一种多态语法机制。
- 多态中涉及到的几个概念
- 上上转型(upcasting):子类à父类,又被称为自动类型转换。
- 向下转型(downcasting):父类à子类,又被称为强制类型转换。【需要加强制类型转换符】
- 无论是向上转型还是向下转型,两种类型之间必须要有继承关系,如果没有继承关系,程序是无法编译通过的。
- 什么时候需要向下转型?
- 当调用的方法或者访问的属性是子类型中特有的,在父类型当中不存在,必须进行向下转型。
- 无论是向上转型还是向下转型,转换的都只是引用的类型,不可能转换对象的类型。【对象从new出来就在堆里面存放着,不可能改变对象的类型】。
- Java程序执行的过程分为编译期和运行期,编译期Java编译器只检查语法,重要语法规则:引用 “点”的时候只能点出来本类或者本类的父类的可见的属性或方法(这里的本类指引用所属的类型,不是引用指向的对象的类型,这里是静态绑定,因为编译期程序并没有运行实际上并不知道引用指向的是个什么对象)。运行期间,首先看看引用指向的实际是一个什么对象(因为Java语法规则允许父类引用指向子类对象),然后从实际对象所属的类型的类体中开始往父类型的方向查找。
- 可以调用编译类型所有的成员,但不能调用子类中特有的成员,因为在编译阶段能调用哪些成员是由编译类型来决定的【也可以称为静态绑定】。
- 如果用父类型的引用指向子类对象,再调用这个引用某个方法的时候执行的过程当中,先看父类引用指向的是一个什么对象,指向哪个类的对象,在运行阶段首先去哪个类中查找有没有该方法。【并不是说父类的引用去调用的某个方法就开始从父类中开始查找某个方法】。
- 以下4行代码是main方法中连续的4行代码,其中Student类继承Person类。
int a = 10; //a指向的存储空间保存的是10,也可以说变量a中保存的是10
long b = a; //把a中保存的10复制了一份,放到变量b指向的存储空间【小类型向大类型转换,称为自动类型转换,编译器允许这种语法机制】
Student s = new Student();//s指向的存储空间保存了一个地址,这个地址指向了一个Student类对象【s保存的是Student类对象的内存地址,可以说s指向了一个Student类对象】
Person p = s;//把s中保存的内存地址复制了一份,放到了p指向的存储空间,所以p保存的也是Student类对象的内存地址【也可以说p指向了一个Student类对象,因为p是父类型的引用,指向了子类型的对象,称为“向上转型”编译器允许这种语法机制】
- 引用 “点”的时候只能点出来本类或者本类的父类中的可见的属性或方法(本类指的是引用定义时的类型不是引用指向的对象的类型,因为编译期是静态绑定,程序没有运行并不知道这里的引用指向的是一个什么类型的对象。)【静态绑定成功的前提条件是,引用定义时的类型中得有这个属性或方法才能绑定成功,绑定成功之后才有后续的运行】。
- //Person是父类Student是子类
Student s = new Student();//用s去调用属性也好,方法也好毋庸置疑都是从Student类中开始寻找
Person p = s;//用p去调用属性是从Person类开始寻找【因为引用p定义时的类型是Person类型的】,用引用p去调用方法是从Student类中开始寻找【因为引用p指向的实际对象是一个Student类型的对象】
System.out.println(((Person)s).getName());//因为这个s指向的是Student类型的对象,即使已经将Student类型的引用上转型为Person类型的引用,但是底层还是Student类型的对象,所以就同理父类型的引用指向子类型的对象,所以用父类型的引用去调用getName方法的时候还是从Student类开始寻找。
System.out.println(((Person)s).name);//因为这个s指向的是Student类型的对象,将s上转型为Person类型的引用,该引用再去调用name方法的时候是从Person类开始寻找【因为已经将Student类型的引用上转型为Person类型的引用】。
- 找方法和找属性都是从编译类型开始往父类的方向寻找
- 找属性:从编译类型开始往父类的方向寻找,一旦找到立马结束寻找。【但遵循访问权限,如果可以访问正常运行,如果不可见,编译报错】
- 找方法:从编译类型中开始往父类的方向寻找,如果编译类型类体中有方法重载,根据调用方法时的实参,从编译类型确定是调用的某个方法,这个方法一旦确定不可更改。如果要是编译类型类体中没有以实参类型为形式参数的方法,【哪怕子类中有以该实参类型为形式参数的方法确定调用某个方法的时候不会去子类中寻找,如果父类中该方法不可见,因为方法可以重载,所以按照参数的亲近成都接着从父类找别的方法】那就去找和实参类型最亲近的并且能兼容实参类型的形式参数的方法。然后在执行的时候,从运行类型开始找有没有这个方法。如果没有接着往父类的方向寻找, 一旦找到立马结束寻找【但遵循访问权限,如果可以访问正常运行,如果不可见,编译报错】。如果运行类型重写了这个方法那就调用运行类型的。
- Java引用变量有两种类型,一个是编译类型,一个是运行类型,编译类型是由声明该变量时使用的类型所决定的,运行类型是由该变量指向的对象的类型决定的。
- Dog d = new Dog();
d.m(d);//找的是m (Dog m)方法,形参类型为Dog类型,如果要是没有找到以Dog类型为形参的方法,那么就找和Dog类型最亲近的类型,并且能兼容Dog类型的形式参数的方法。
d.m((Animal)d);//找的是m (Animal a)方法,形参类型为Animal类型Animal,这里相当于是父类型的引用指向子类型的对象,但是在选择方法的时候选的是编译类型(Animal)的形式参数的方法,不是运行类型。【d.m((Animal)d);这里可以认为是把编译类型改成Animal了但是运行类型还是Dog】。
- Dog dog = (Dog)new Animal();【代码抛出运行时异常:向下转型的时候需要首先确定,转型前的对象是一个目标类型的对象,否则会抛出异常。也就是说该对象是从子类转上去的才可以从父类转下来】底层new了一个父类对象直接强转成子类对象,虽然编译器通过了,但是程序是分编译期和运行期的,编译期他不会去执行“等号”后面的代码,他只检查语法【(Dog)new Animal()因为有继承关系,所以把父类对象可以强转成子类对象,本类的引用指向本类的对象,编译器语法通过】,但是在运行的时候一看这不是一条“狗”对象自然会抛出运行时异常(java.lang.ClassCastException)。【所以不用考虑确定执行哪个方法的时候,从Dog类开始寻找,而实际运行的时候是从Animal类开始找确定运行的这个方法。因为子类可能新增方法,父类引用没有当然也不可能调用子类的新增方法,所以Java的这种设计思考起来也是挺合理的】
- 多态向上转型
本质:父类的引用指向子类的对象
语法:父类类型 引用名 = new 子类类型();
特点:编译类型看左边运行类型看右边【编译确定调用的方法是从编译类型开始寻找,运行的时候是从运行类型开始寻找】
可以调用父类中所有的成员(需遵循访问权限)
不能调用子类中特有的成员。【想要调用子类特有的成员则需要向下转型】
最终运行效果看子类的实现。
向下转型:
本质:子类类型 引用名 = (子类类型)父类引用;
instanceof语法结构:instanceof用于判断某个对象的运行类型是否为xx类型或者xx类型的子类型。
语法格式:(引用 instanceof 数据类型名)
以上运算符执行结果是布尔类型,结果可能是true/false
假设: (a instanceof Animal)
a引用指向的对象是一个Animal类型或者Animal类型的子类型返回true,否则返回false。
【某个对象的运行类型和xx类型之间要有继承关系才可以使用instanceof关键字比较,没有任何关系的两个类直接使用instanceof会编译报错】
- 多态参数
方法定义的形参为父类类型,实参类型允许为子类类型。【这样也相当于是父类引用指向子类对象】。
- 面向对象的核心:定义好类,然后类实例化对象,给一个环境驱使一下,让各个对象协作起来形成一个系统。
- 多态的作用
- 降低程序的耦合度,提高程序的扩展力
- 能使用多态尽量使用多态
- 父类型的引用指向子类型的对象。