6.1 封装
6.1.1 封装概述
1、为什么要封装?
我要用洗衣机,只需要按一下开关和洗涤模式就可以了。有必要了解洗衣机内部的结构吗?有必要碰电动机吗?
- 面向对象编程语言是对客观世界的模拟,客观世界里每一个事物的内部信息都是隐藏在对象内部的,外界无法直接操作和修改,只能通过指定的方式进行访问和修改。封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问。
- 适当的封装可以让代码更容易理解与维护,也加强了代码的安全性。
- 通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。
- 随着我们系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“高内聚、低耦合”:
高内聚:相关的功能和数据尽可能地组织在同一个类中,各个类内部数据操作细节自己完成,以确保其独立性。
低耦合:各个类之间的依赖关系应尽可能低。也就是说,一个类的变化应尽量减少对其他类的影响。有助于提高软件的可扩展性、可复用性和灵活性。
2、如何实现封装?
实现封装就是指控制类或成员的可见性范围?这就需要依赖访问控制修饰符,也称为权限修饰符来控制。
权限修饰符:private,缺省,protected,public
修饰符 | 本类 | 本包 | 其他包子类 | 其他包非子类 |
---|---|---|---|---|
private | √ | × | × | × |
缺省 | √ | √ | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
外部类:public和缺省
成员变量、成员方法、构造器、成员内部类:private,缺省,protected,public
6.1.2 成员变量/属性私有化
在面向对象编程中,将成员变量(也称为属性)私有化是一种常见的做法。这样做的目的是保护数据,防止外部直接访问和修改,以确保数据的完整性和安全性。同时,通过提供公共的getter和setter方法来访问和修改这些私有变量,可以更好地控制对这些变量的访问和修改。
1、成员变量封装的目的
- 隐藏类的实现细节
- 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。
2、实现步骤
1.使用 private
修饰成员变量
private 数据类型 变量名 ;
代码如下:
public class Person {
private String name;
private int age;
private boolean marry;
}
2.提供 getXxx
方法 / setXxx
方法,可以访问成员变量,代码如下:
public class Person {
private String name;
private int age;
private boolean marry;public void setName(String n) {
name = n;
}public String getName() {
return name;
}public void setAge(int a) {
age = a;
}public int getAge() {
return age;
}
public void setMarry(boolean m){
marry = m;
}
public boolean isMarry(){
return marry;
}
}
3. 测试
package com.haogu.encapsulation;
public class TestPerson {
public static void main(String[] args) {
Person p = new Person();
//实例变量私有化,跨类是无法直接使用的
/* p.name = "张三";
p.age = 23;
p.marry = true;*/
p.setName("张三");
System.out.println("p.name = " + p.getName());
p.setAge(23);
System.out.println("p.age = " + p.getAge());
p.setMarry(true);
System.out.println("p.marry = " + p.isMarry());
}
}
6.1.3 IDEA自动生成set/get方法模板
1、如何解决局部变量与实例变量同名问题
当局部变量与实例变量(非静态成员变量)同名时,在实例变量必须前面加“this.”
public class Employee {
private String name;
private int age;
private boolean marry;
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;
}
public boolean isMarry() {
return marry;
}
public void setMarry(boolean marry) {
this.marry = marry;
}
}
public class TestEmployee {
public static void main(String[] args) {
Employee e = new Employee();
e.setName("张三");
System.out.println("e.name = " + e.getName());
e.setAge(23);
System.out.println("e.age = " + e.getAge());
e.setMarry(true);
System.out.println("e.marry = " + e.isMarry());
}
}
2、IDEA自动生成set/get方法模板
- 大部分键盘模式按Alt + Insert键。
- 部分键盘模式需要按Alt + Insert + Fn键。
6.2 继承(Inheritance)
6.2.1 继承的概述
继承有延续(下一代延续上一代的基因、财富)、扩展(下一代和上一代又有所不同)的意思。
Java中的继承
如图所示:
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成某种关系。如图所示:
其中,多个类可以称为子类,也叫派生类;多个类抽取出来的这个类称为父类、超类(superclass)或者基类。
继承描述的是事物之间的所属关系,这种关系是:is-a
的关系。例如,图中猫属于动物,狗也属于动物。可见,父类更通用或更一般,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。
继承的好处
- 提高代码的复用性。
- 提高代码的扩展性。
- 表示类与类之间的 is-a 关系
6.2.2 继承的语法格式
通过 extends
关键字,可以声明一个子类继承另外一个父类,定义格式如下:
【修饰符】 class 父类 {
...
}【修饰符】 class 子类 extends 父类 {
...
}
1、父类
/*
* 定义动物类Animal,做为父类
*/
public class Animal {
// 定义name属性
String name;
// 定义age属性
int age;
// 定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的"
+ name + "在吃东西");
}
}
2、子类
/*
* 定义猫类Cat 继承 动物类Animal
*/
public class Cat extends Animal {
int count;//记录每只猫抓的老鼠数量
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
count++;
System.out.println("抓老鼠,已经抓了"
+ count + "只老鼠");
}
}
3、测试类
public class TestCat {
public static void main(String[] args) {
// 创建一个猫类对象
Cat cat = new Cat();
// 为该猫类对象的name属性进行赋值
cat.name = "Tom";
// 为该猫类对象的age属性进行赋值
cat.age = 2;
// 调用该猫继承来的eat()方法
cat.eat();
// 调用该猫的catchMouse()方法
cat.catchMouse();
cat.catchMouse();
cat.catchMouse();
}
}
6.2.3 继承的特点
1、子类会继承父类所有的实例变量和实例方法
从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。
- 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
- 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。
所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板。
2、Java值支持单继承,不支持多重继承
public class A{}
class B extends A{}
//一个类只能有一个父类,不可以有多个直接父类。
class C extends B{} //ok
class C extends A,B... //error
3、Java支持多层继承(继承体系)
class A{}
class B extends A{}
class C extends B{}
顶层父类是Object类。所有的类默认继承Object,作为父类。
4、一个父类可以同时拥有多个子类
class A{}
class B extends A{}
class D extends A{}
class E extends A{}
6.2.4 IDEA中如何查看继承关系
1、子类和父类是一种相对的概念
例如:B类对于A来说是子类,但是对于C类来说是父类
2、查看继承关系的快捷键
例如:选择A类名,按Ctrl + H就会显示A类的继承树。
:A类的父类和子类
:A类的父类
:A类的所有子类
例如:在类继承目录树中选中某个类,比如C类,按Ctrl+ Alt+U就会用图形化方式显示C类的继承祖宗
6.2.5 权限修饰符限制
权限修饰符:private,缺省,protected,public
修饰符 | 本类 | 本包 | 其他包子类 | 其他包非子类 |
---|---|---|---|---|
private | √ | × | × | × |
缺省 | √ | √(本包子类非子类都可见) | × | × |
protected | √ | √(本包子类非子类都可见) | √(其他包仅限于子类中可见) | × |
public | √ | √ | √ | √ |
外部类:public和缺省
成员变量、成员方法等:private,缺省,protected,public
1、外部类要挎包使用必须是public,否则仅限于本包使用
(1)外部类的权限修饰符如果缺省,本包使用没问题
(2)外部类的权限修饰符如果缺省,跨包使用有问题
2、成员的权限修饰符
(1)本包下使用:成员的权限修饰符可以是public、protected、缺省
(2)跨包下使用:当成员需要被跨包访问时,这意味着要使用public
或protected
(对于子类而言)。
3、父类成员变量私有化(private)
子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问。如图所示:
父类代码:
public class Person {
private String name;
private int 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;
}
public String getInfo(){
return "姓名:" + name + ",年龄:" + age;
}
}
子类代码:
public class Student extends Person {
private int score;
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public String getInfo(){
// return "姓名:" + name + ",年龄:" + age;
//在子类中不能直接使用父类私有的name和age
return "姓名:" + getName() + ",年龄:" + getAge();
}
}
测试类:
public class TestStudent {
public static void main(String[] args) {
Student student = new Student();
student.setName("张三");
student.setAge(23);
student.setScore(89);
System.out.println(student.getInfo());
}
}
IDEA在Debug模式下查看学生对象信息:
6.2.6 方法重写(Override)
我们说父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于子类,该怎么办呢?我们可以进行方法重写 (Override)
1、方法重写
比如新的手机增加来电显示头像的功能,代码如下:
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("显示头像");
}
}
public class TestOverride {
public static void main(String[] args) {
// 创建子类对象
Smartphone sp = new Smartphone();
// 调用父类继承而来的方法
sp.call();
// 调用子类重写的方法
sp.showNum();
}
}
2、在子类中如何调用父类被重写的方法
//smartphone:智能手机
public class Smartphone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
}
3、IDEA重写方法快捷键:Ctrl + O
//smartphone:智能手机
public class Smartphone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
@Override
public void call() {
super.call();
System.out.println("视频通话");
}
}
@Override:写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。
4、重写方法要求
1.必须保证父子类之间重写方法的名称相同。
2.必须保证父子类之间重写方法的参数列表也完全相同。
5、方法的重载和方法的重写的区别
方法重载(Overload):
- 发生在同一个类中。
- 方法名相同,但参数列表(数量、类型、顺序)不同。
- 实现功能的多样性,与返回类型无关。
方法重写(Override):
- 发生在子类与父类之间。
- 方法名、参数列表完全相同,返回类型相同或为其子类型。
- 子类重新实现父类方法,行为可能与父类不同。
- 子类方法的访问级别不能低于父类方法,异常处理也要遵循一定规则。
简而言之,重载是同一个类中方法名相同但参数不同,重写是子类与父类方法名、参数都相同但实现不同。
public class TestOverloadOverride {
public static void main(String[] args) {
Son s = new Son();
s.method(1);//只有一个形式的method方法
Daughter d = new Daughter();
d.method(1);
d.method(1,2);//有两个形式的method方法
}
}
class Father{
public void method(int i){
System.out.println("Father.method");
}
}
class Son extends Father{
public void method(int i){//重写
System.out.println("Son.method");
}
}
class Daughter extends Father{
public void method(int i,int j){//重载
System.out.println("Daughter.method");
}
}
6.3 多态(Polymorphism)
多态是继封装、继承之后,面向对象的第三大特性。它表示一个对象具有多重特征,可以在特定的情况下表现出不同的状态。
生活中,比如求面积的功能,圆、矩形、三角形实现起来是不一样的。跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。那么此时就会出现各种子类的类型。
6.3.1 多态解决什么样的问题
有的时候,我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
案例:
(1)声明一个Dog类,包含public void eat()方法,输出“狗狗啃骨头”
(2)声明一个Cat类,包含public void eat()方法,输出“猫咪吃鱼仔”
(3)声明一个Person类,
- 包含宠物属性
- 包含领养宠物方法 public void adopt(宠物类型 pet)
- 包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
问题:
1、从养狗切换到养猫怎么办? 修改代码把Dog修改为养猫?
2、或者有的人养狗,有的人养猫怎么办?
3、要是同时养多个狗,或猫怎么办?
4、要是还有更多其他宠物类型怎么办?
public class Dog {
public void eat(){
System.out.println("狗狗啃骨头");
}
}
public class Cat {
public void eat(){
System.out.println("猫咪吃鱼仔");
}
}
public class Person {
private Dog dog;
//adopt:领养
public void adopt(Dog dog){
this.dog = dog;
}
//feed:喂食
public void feed(){
if(dog != null){
dog.eat();
}
}
/*
问题:
1、从养狗切换到养猫怎么办?
修改代码把Dog修改为养猫?
2、或者有的人养狗,有的人养猫怎么办?
3、要是同时养多个狗,或猫怎么办?
4、要是还有更多其他宠物类型怎么办?
如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
*/
}
6.3.2 多态的形式和体现
1、多态引用
Java规定父类类型的变量可以接收子类类型的对象,这一点从逻辑上也是说得通的。
父类类型 变量名 = 子类对象;
父类类型:指子类继承的父类类型。
所以说继承是多态的前提
2、多态引用的表现
表现:编译时类型与运行时类型不一致,编译时看“父类”,运行时看“子类”。
3、多态引用的好处和弊端
弊端:编译时,只能调用父类声明的方法,不能调用子类扩展的方法;
好处:运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
4、多态演示
让Dog和Cat都继承Pet宠物类。
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("看家");
}
}
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重写的方法
}
}
6.3.3 应用多态解决的问题
1、声明变量是父类类型,变量赋值子类对象
- 方法的形参是父类类型,调用方法的实参是子类对象
- 实例变量声明父类类型,实际存储的是子类对象
public class OnePersonOnePet {
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
}
public class TestOnePersonOnePet {
public static void main(String[] args) {
OnePersonOnePet person = new OnePersonOnePet();
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();
}
}
2、数组元素是父类类型,元素对象是子类对象
public class OnePersonManyPets {
private Pet[] pets;//数组元素类型是父类类型,元素存储的是子类对象
public void adopt(Pet[] pets) {
this.pets = pets;
}
public void feed() {
for (int i = 0; i < pets.length; i++) {
pets[i].eat();//pets[i]实际引用的对象类型不同,执行的eat方法也不同
}
}
}
public class TestPets {
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("雪球");
OnePersonManyPets person = new OnePersonManyPets();
person.adopt(pets);
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();
}
}
6.3.4 向上转型与向下转型
首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
这个和基本数据类型的转换是不同的。基本数据类型是把数据值copy了一份,相当于有两种数据类型的值。而对象的赋值不会产生两个对象。
1、为什么要类型转换呢?
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过。
-
向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
-
此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
-
但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
-
此时,一定是安全的,而且也是自动完成的
-
-
向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型
-
此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
-
但是,运行时,仍然是对象本身的类型
-
不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
-
2、如何向上转型与向下转型
向上转型:自动完成
向下转型:(子类类型)父类变量
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之间是没有继承关系的
}
}
3、instanceof关键字
为了避免ClassCastException的发生,Java提供了 instanceof
关键字,给引用变量做类型的校验,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
变量/匿名对象 instanceof 数据类型
那么,哪些instanceof判断会返回true呢?
-
变量/匿名对象的编译时类型 与 instanceof后面数据类型是直系亲属关系才可以比较
-
变量/匿名对象的运行时类型<= instanceof后面数据类型,才为true
示例代码:
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();
}
}
}
}
6.3.5 虚方法
在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
当我们通过“对象xx.方法”的形式调用一个虚方法时,要如何确定它具体执行哪个方法呢?
(1)静态分派:先看这个对象xx的编译时类型,在这个对象的编译时类型中找到能匹配的方法
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
(2)动态绑定:再看这个对象xx的运行时类型,如果这个对象xx的运行时类重写了刚刚找到的那个匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个匹配的方法
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
}
class MySub extends MyClass{
public void method(Father d) {
System.out.println("sub--father");
}
public void method(Daughter d) {
System.out.println("daughter");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestVirtualMethod {
public static void main(String[] args) {
MyClass my = new MySub();
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
my.method(f);//sub--
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参f的编译时类型是Father,形参(Father f) 、(Son s)
最匹配的是public void method(Father f)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Father f)进行重写
发现有重写,如果有重写,就执行重写的
public void method(Father d) {
System.out.println("sub--");
}
*/
my.method(s);//son
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参s的编译时类型是Son,形参(Father f) 、(Son s)
最匹配的是public void method(Son s)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Son s)进行重写
发现没有重写,如果没有重写,就执行刚刚父类中找到的方法
*/
my.method(d);//sub--
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参d的编译时类型是Daughter,形参(Father f) 、(Son s)
最匹配的是public void method(Father f)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Father f)进行重写
发现有重写,如果有重写,就执行重写的
public void method(Father d) {
System.out.println("sub--");
}
*/
}
}
6.3.6 成员变量没有多态一说
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;
}
6.4 实例初始化
6.4.1 构造器
我们发现我们new完对象时,所有成员变量都是默认值,如果我们需要赋别的值,需要挨个为它们再赋值,太麻烦了。我们能不能在new对象时,直接为当前对象的某个或所有成员变量直接赋值呢。
可以,Java给我们提供了构造器(Constructor)。
1、构造器的作用
new对象,并在new对象的时候为实例变量赋值。
2、构造器的语法格式
构造器又称为构造方法,那是因为它长的很像方法。但是和方法还是有所区别的。
【修饰符】 class 类名{
【修饰符】 构造器名(){
// 实例初始化代码
}
【修饰符】 构造器名(参数列表){
// 实例初始化代码
}
}
public class Student {
private String name;
private int age;
// 无参构造
public Student() {}
// 有参构造
public Student(String name,int age) {
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;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}
注意事项:
- 构造器名必须与它所在的类名必须相同。
- 它没有返回值,所以不需要返回值类型,甚至不需要void
- 如果你不提供构造器,系统会给出无参数构造器,并且该构造器的修饰符默认与类的修饰符相同
- 如果你提供了构造器,系统将不再提供无参数构造器,除非你自己定义。
- 构造器是可以重载的,既可以定义参数,也可以不定义参数。
- 构造器的修饰符只能是权限修饰符,不能被其他任何修饰
public class TestStudent {
public static void main(String[] args) {
//调用无参构造创建学生对象
Student s1 = new Student();
//调用有参构造创建学生对象
Student s2 = new Student("张三",23);
System.out.println(s1.getInfo());
System.out.println(s2.getInfo());
}
}
3、同一个类中的构造器互相调用
- this():调用本类的无参构造
- this(实参列表):调用本类的有参构造
- this()和this(实参列表)只能出现在构造器首行
- 不能出现递归调用
public class Student {
private String name;
private int age;
// 无参构造
public Student() {
// this("",18);//调用本类有参构造
}
// 有参构造
public Student(String name,int age) {
this();//调用本类无参构造
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;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}
4、继承时构造器如何处理
-
子类继承父类时,不会继承父类的构造器。只能通过super()或super(实参列表)的方式调用父类的构造器。
-
super();:子类构造器中一定会调用父类的构造器,默认调用父类的无参构造,super();可以省略。
-
super(实参列表);:如果父类没有无参构造或者有无参构造但是子类就是想要调用父类的有参构造,则必须使用super(实参列表);的语句。
-
super()和super(实参列表)都只能出现在子类构造器的首行
public class Employee {
private String name;
private int age;
private double salary;
public Employee() {
System.out.println("父类Employee无参构造");
}
public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
System.out.println("父类Employee有参构造");
}
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;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public String getInfo(){
return "姓名:" + name + ",年龄:" + age +",薪资:" + salary;
}
}
public class Manager extends Employee{
private double bonusRate;
public Manager() {
super();//可以省略
}
public Manager(String name, int age, double salary, double bonusRate) {
super(name, age, salary);//调用父类的有参构造
this.bonusRate = bonusRate;
}
public double getBonusRate() {
return bonusRate;
}
public void setBonusRate(double bonusRate) {
this.bonusRate = bonusRate;
}
@Override
public String getInfo() {
return super.getInfo() +",奖金比例:" + bonusRate;
}
}
public class TestEmployee {
public static void main(String[] args) {
Manager m1 = new Manager();
System.out.println(m1.getInfo());
Manager m2 = new Manager("张三",23,20000,0.1);
System.out.println(m2.getInfo());
}
}
形式一:
class A{
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
//A类和B类都是默认有一个无参构造,B类的默认无参构造中还会默认调用A类的默认无参构造
//但是因为都是默认的,没有打印语句,看不出来
}
}
形式二 :
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类默认有一个无参构造,
//B类的默认无参构造中会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"
}
}
形式三 :
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中虽然没有写super(),但是仍然会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}
形式四:
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中明确写了super(),表示调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}
形式五:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test05{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造没有写super(...),表示默认调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}
形式六:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test06{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造明确写super(),表示调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}
形式七:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(int a){
super(a);
System.out.println("B类有参构造器");
}
}
class Test07{
public static void main(String[] args){
B b = new B(10);
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个有参构造,
//B类的有参构造明确写super(a),表示调用A类的有参构造
//会打印“A类有参构造器"和"B类有参构造器"
}
}
形式八:
class A{
A(){
System.out.println("A类无参构造器");
}
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();//可以省略,调用父类的无参构造
System.out.println("B类无参构造器");
}
B(int a){
super(a);//调用父类有参构造
System.out.println("B类有参构造器");
}
}
class Test8{
public static void main(String[] args){
B b1 = new B();
B b2 = new B(10);
}
}
5、IDEA生成构造器:Alt + Insert
6、IDEA查看构造器和方法形参列表快捷键:Ctrl + P
6.4.2 非静态代码块(了解)
1、非静态代码块的作用
和构造器一样,也是用于实例变量的初始化等操作。
2、非静态代码块的意义
如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。
3、非静态代码块的执行特点
所有非静态代码块中代码都是在new对象时自动执行,并且一定是先于构造器的代码执行。
4、非静态代码块的语法格式
【修饰符】 class 类{
{
非静态代码块
}
【修饰符】 构造器名(){
// 实例初始化代码
}
【修饰符】 构造器名(参数列表){
// 实例初始化代码
}
}
5、非静态代码块的应用
案例:
(1)声明User类,
-
包含属性:username(String类型),password(String类型),registrationTime(long类型),私有化
-
包含get/set方法,其中registrationTime没有set方法
-
包含无参构造,
-
输出“新用户注册”,
-
registrationTime赋值为当前系统时间,
-
username就默认为当前系统时间值,
-
password默认为“123456”
-
-
包含有参构造(String username, String password),
-
输出“新用户注册”,
-
registrationTime赋值为当前系统时间,
-
username和password由参数赋值
-
-
包含public String getInfo()方法,返回:“用户名:xx,密码:xx,注册时间:xx”
(2)编写测试类,测试类main方法的代码如下:
public static void main(String[] args) {
User u1 = new User();
System.out.println(u1.getInfo());
User u2 = new User("lee","8888");
System.out.println(u2.getInfo());
}
如果不用非静态代码块,User类是这样的:
public class User {
private String username;
private String password;
private long registrationTime;
public User() {
System.out.println("新用户注册");
registrationTime = System.currentTimeMillis();
username = registrationTime+"";
password = "123456";
}
public User(String username,String password) {
System.out.println("新用户注册");
registrationTime = System.currentTimeMillis();
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public long getRegistrationTime() {
return registrationTime;
}
public String getInfo(){
return "用户名:" + username + ",密码:" + password + ",注册时间:" + registrationTime;
}
}
如果提取构造器公共代码到非静态代码块,User类是这样的:
public class User {
private String username;
private String password;
private long registrationTime;
{
System.out.println("新用户注册");
registrationTime = System.currentTimeMillis();
}
public User() {
username = registrationTime+"";
password = "123456";
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public long getRegistrationTime() {
return registrationTime;
}
public String getInfo(){
return "用户名:" + username + ",密码:" + password + ",注册时间:" + registrationTime;
}
}
6.4.3 实例初始化过程(了解)
1、实例初始化的目的
实例初始化的过程其实就是在new对象的过程中为实例变量赋有效初始值的过程
2、实例初始化相关代码
在new对象的过程中给实例变量赋初始值可以通过以下3个部分的代码完成:
(1)实例变量直接初始化
(2)非静态代码块
(3)构造器
当然,如果没有编写上面3个部分的任何代码,那么实例变量也有默认值。
3、实例初始化方法
实际上我们编写的代码在编译时,会自动处理代码,整理出一个或多个的<init>(...)实例初始化方法。一个类有几个实例初始化方法,由这个类就有几个构造器决定。
实例初始化方法的方法体,由4部分构成:
(1)super()或super(实参列表)
-
这里选择哪个,看原来构造器首行是super()还是super(实参列表)
-
如果原来构造器首行是this()或this(实参列表),那么就取对应构造器首行的super()或super(实参列表)
-
如果原来构造器首行既没写this()或this(实参列表),也没写super()或super(实参列表) ,默认就是super()
(2)非静态实例变量的显示赋值语句
(3)非静态代码块
(4)对应构造器中剩下的的代码
特别说明:其中(2)和(3)是按顺序合并的,(1)一定在最前面(4)一定在最后面
4、实例初始化执行特点
- 创建对象时,才会执行
- 每new一个对象,都会完成该对象的实例初始化
- 调用哪个构造器,就是执行它对应的<init>实例初始化方法
- 子类super()还是super(实参列表)实例初始化方法中的super()或super(实参列表) 不仅仅代表父类的构造器代码了,而是代表父类构造器对应的实例初始化方法。
5、演示父类实例初始化
public class Father {
private int a = 1;
public Father(){
System.out.println("Father类的无参构造");
}
public Father(int a, int b){
System.out.println("Father类的有参构造");
this.a = a;
this.b = b;
}
{
System.out.println("Father类的非静态代码块1,a = " + a);
System.out.println("Father类的非静态代码块1,b = " + this.b);
}
private int b = 1;
{
System.out.println("Father类的非静态代码块2,a = " + a);
System.out.println("Father类的非静态代码块2,b = " + b);
}
public String getInfo(){
return "a = " + a + ",b = " + b;
}
}
public class TestFather {
public static void main(String[] args) {
Father f1 = new Father();
System.out.println(f1.getInfo());
System.out.println("-----------------------");
Father f2 = new Father(10,10);
System.out.println(f2.getInfo());
}
}
6、演示子类实例初始化
public class Son extends Father {
private int c = 1;
{
System.out.println("Son类的非静态代码块,c = " + c);
}
public Son() {
System.out.println("Son类的无参构造");
}
public Son(int a, int b, int c) {
super(a, b);
this.c = c;
System.out.println("Son类的有参构造");
}
@Override
public String getInfo() {
return super.getInfo() + ",c = " + c;
}
}
public class TestSon {
public static void main(String[] args) {
Son s1 = new Son();
System.out.println(s1.getInfo());
System.out.println("---------------");
Son s2 = new Son(10,10,10);
System.out.println(s2.getInfo());
}
}
6.5 关键字和API
6.5.1 this和super关键字
1.this和super的意义
this:当前对象
-
在构造器和非静态代码块中,表示正在new的对象
-
在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
无论是this和super都是和对象有关的。
2.this和super的使用格式
this:当前对象
-
在构造器和非静态代码块中,表示正在new的对象
-
在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
无论是this和super都是和对象有关的。
3.避免子类和父类声明重名的成员变量
特别说明:应该避免子类声明和父类重名的成员变量
因为,子类会继承父类所有的成员变量,所以:
-
如果重名的成员变量表示相同的意义,就无需重复声明
-
如果重名的成员变量表示不同的意义,会引起歧义
在阿里的开发规范等文档中都做出明确说明
4.解决成员变量重名问题
-
如果实例变量与局部变量重名,可以在实例变量前面加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找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
-
如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)
-
5.解决成员方法重写后调用问题
-
如果子类没有重写父类的方法,只有权限修饰符运行,在子类中完全可以直接调用父类的方法;
-
如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则默认调用的子类重写的方法
public class Test{
public static void main(String[] args){
Son s = new Son();
s.test();
Daughter d = new Daughter();
d.test();
}
}
class Father{
protected int num = 10;
public int getNum(){
return num;
}
}
class Son extends Father{
private int num = 20;
public void test(){
System.out.println(getNum());//10 本类没有找父类,执行父类中的getNum()
System.out.println(this.getNum());//10 本类没有找父类,执行父类中的getNum()
System.out.println(super.getNum());//10 本类没有找父类,执行父类中的getNum()
}
}
class Daughter extends Father{
private int num = 20;
@Override
public int getNum(){
return num;
}
public void test(){
System.out.println(getNum());//20 先找本类,执行本类的getNum()
System.out.println(this.getNum());//20 先找本类,执行本类的getNum()
System.out.println(super.getNum());//10 直接找父类,执行父类中的getNum()
}
}
总结:
-
方法前面没有super.和this.
-
先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
-
-
方法前面有this.
-
先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
-
-
方法前面有super.
-
从当前子类的直接父类找,如果没有,继续往上追溯
-
6.5.2 native关键字
1.native的意义
native:本地的,原生的
2.native的语法
native只能修饰方法,表示这个方法的方法体代码不是用Java语言实现的,而是由C/C++语言编写的。但是对于Java程序员来说,可以当做Java的方法一样去正常调用它,或者子类重写它。
JVM内存的管理:
区域名称 | 作用 |
---|---|
程序计数器 | 程序计数器是CPU中的寄存器,它包含每一个线程下一条要执行的指令的地址 |
本地方法栈 | 当程序中调用了native的本地方法时,本地方法执行期间的内存区域 |
方法区 | 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 |
堆内存 | 存储对象(包括数组对象),new来创建的,都存储在堆内存。 |
虚拟机栈 | 用于存储正在执行的每个Java方法的局部变量表等。局部变量表存放了编译期可知长度的各种基本数据类型、对象引用,方法执行完,自动释放。 |
6.5.3 final关键字
1.final的意义
final:最终的,不可更改的
2.final修饰类
表示这个类不能被继承,没有子类
final class Eunuch{//太监类
}
class Son extends Eunuch{//错误
}
3.final修饰方法
表示这个方法不能被子类重写
class Father{
public final void method(){
System.out.println("father");
}
}
class Son extends Father{
public void method(){//错误
System.out.println("son");
}
}
4.final修饰变量
final修饰某个变量(成员变量或局部变量),表示它的值就不能被修改,即常量,常量名建议使用大写字母。
如果某个成员变量用final修饰后,没有set方法,并且必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)
public class TestFinal {
public static void main(String[] args){
final int MIN_SCORE = 0;
final int MAX_SCORE = 100;
MyDate m1 = new MyDate();
System.out.println(m1.getInfo());
MyDate m2 = new MyDate(2022,2,14);
System.out.println(m2.getInfo());
}
}
class MyDate{
//没有set方法,必须有显示赋值的代码
private final int year;
private final int month;
private final int day;
public MyDate(){
year = 1970;
month = 1;
day = 1;
}
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public int getYear() {
return year;
}
public int getMonth() {
return month;
}
public int getDay() {
return day;
}
public String getInfo(){
return year + "年" + month + "月" + day + "日";
}
}
6.5.4 Object 根父亲
1、如何理解根父亲
类 java.lang.Object
是类层次结构的根类,即所有类的父类。每个类都使用 Object
作为超类。
-
Object类型的变量与除Object以外的任意引用数据类型的对象都多态引用
-
所有对象(包括数组)都实现这个类的方法。
-
如果一个类没有特别指定父类,那么默认则继承自Object类。例如:
public class MyClass /*extends Object*/ {
// ...
}
2、object类的其中五个方法
API(Application Programming Interface),应用程序编程接口。Java API是一本程序员的字典
,是JDK中提供给我们使用的类的说明文档。所以我们可以通过查询API的方式,来学习Java提供的类,并得知如何使用它们。在API文档中是无法得知这些类具体是如何实现的,如果要查看具体实现代码,那么我们需要查看src源码。
根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。今天我们主要学习其中的5个:
- toString()
方法签名:public String toString()
①默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"
②通常是建议重写
③如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()
因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。
例如自定义的Person类:
public class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
- getClass()
public final Class<?> getClass():获取对象的运行时类型
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}
- equals()
public boolean equals(Object obj):用于判断当前对象this与指定对象obj是否“相等”
①默认情况下,equals方法的实现等价于与“==”,比较的是对象的地址值
②我们可以选择重写,重写有些要求:
如果重写equals,那么一定要遵循如下几个原则:
a:自反性:x.equals(x)返回true
b:传递性:x.equals(y)为true, y.equals(z)为true,然后x.equals(z)也应该为true
c:一致性:只要参与equals比较的属性值没有修改,那么无论何时调用结果应该一致
d:对称性:x.equals(y)与y.equals(x)结果应该一样
e:非空对象与null的equals一定是false
class User{
private String host;
private String username;
private String password;
public User(String host, String username, String password) {
super();
this.host = host;
this.username = username;
this.password = password;
}
public User() {
super();
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [host=" + host + ", username=" + username + ", password=" + password + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((host == null) ? 0 : host.hashCode());
result = prime * result + ((password == null) ? 0 : password.hashCode());
result = prime * result + ((username == null) ? 0 : username.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (host == null) {
if (other.host != null)
return false;
} else if (!host.equals(other.host))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
}
- hashCode()
public int hashCode():返回每个对象的hash值。
如果重写equals,那么通常会一起重写hashCode()方法,hashCode()方法主要是为了当对象存储到哈希表(后面集合章节学习)等容器中时提高存储和查询性能用的,这是因为关于hashCode有两个常规协定:
-
①如果两个对象的hash值是不同的,那么这两个对象一定不相等;
-
②如果两个对象的hash值是相同的,那么这两个对象不一定相等。
重写equals和hashCode方法时,要保证满足如下要求:
-
①如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;
-
②如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;
-
③如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false
总结:
使用
hashCode()
来比较两个对象是否相等是不合适的,因为确实存在不同的对象具有相同hashCode
值的可能性。这种情况被称为哈希冲突,是哈希表等数据结构必须处理的一种情况。当你需要比较两个对象是否相等时,应该使用
equals()
方法而不是hashCode()
方法。如果两个对象根据
equals()
方法是相等的,那么它们的hashCode()
方法应该返回相同的值,这是Java对象的一个基本要求。但是,反过来并不成立:即使两个对象的hashCode()
值相同,它们也可能不相等。所以,在编写Java代码时,如果你重写了
equals()
方法,通常也应该重写hashCode()
方法,以确保它们的行为一致。这样可以确保当两个对象根据equals()
方法相等时,它们的哈希码也相同,但是,不要仅仅依赖hashCode()
来比较对象的相等性。
- finalize()(了解)
protected void finalize():用于最终清理内存的方法
演示finalize()方法被调用:
package com.haogu.api;
public class TestFinalize {
public static void main(String[] args) throws Throwable{
for (int i=1; i <=10; i++){
MyDemo my = new MyDemo(i);
//每一次循环my就会指向新的对象,那么上次的对象就没有变量引用它了,就成垃圾对象
}
//为了看到垃圾回收器工作,我要加下面的代码,让main方法不那么快结束,因为main结束就会导致JVM退出,GC也会跟着结束。
System.gc();//如果不调用这句代码,GC可能不工作,因为当前内存很充足,GC就觉得不着急回收垃圾对象。
//调用这句代码,会让GC尽快来工作。
Thread.sleep(5000);//单位是毫秒,让当前程序休眠5秒再结束
}
}
class MyDemo{
private int value;
public MyDemo(int value) {
this.value = value;
}
@Override
public String toString() {
return "MyDemo{" + "value=" + value + '}';
}
//重写finalize方法,让大家看一下它的调用效果
@Override
protected void finalize() throws Throwable {
// 正常重写,这里是编写清理系统内存的代码
// 这里写输出语句是为了看到finalize()方法被调用的效果
System.out.println(this+ "轻轻的走了,不带走一段代码....");
}
}
每一个对象的finalize()只会被调用一次,哪怕它多次被标记为垃圾对象。当一个对象没有有效的引用/变量指向它,那么这个对象就是垃圾对象。GC(垃圾回收器)通常会在第一次回收某个垃圾对象之前,先调用一下它的finalize()方法,然后再彻底回收它。但是如果在finalize()方法,这个垃圾对象“复活”了(即在finalize()方法中意外的又有某个引用指向了当前对象,这是要避免的),被“复活”的对象如果再次称为垃圾对象,GC就不再调用它的finalize方法了,避免这个对象称为“僵尸”。
public class TestFinalize {
private static MyDemo[] arr = new MyDemo[10];
private static int total;
public static void add(MyDemo demo){
arr[total++] = demo;
}
public static void main(String[] args) throws Throwable{
for (int i=1; i <=10; i++){
MyDemo my = new MyDemo(i);
//每一次循环my就会指向新的对象,那么上次的对象就没有变量引用它了,就成垃圾对象
}
//为了看到垃圾回收器工作,我要加下面的代码,让main方法不那么快结束,因为main结束就会导致JVM退出,GC也会跟着结束。
System.gc();//如果不调用这句代码,GC可能不工作,因为当前内存很充足,GC就觉得不着急回收垃圾对象。
//调用这句代码,会让GC尽快来工作。
Thread.sleep(5000);//单位是毫秒,让当前程序休眠5秒再结束
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);//MyDemo的对象还在,没有被回收掉,因为在回收过程中被复活了
}
for (int i = 0; i < arr.length; i++) {
arr[i] = null;//让这些元素不引用MyDemo的对象,这些对象再次称为垃圾对象
System.out.println(arr[i]);
}
arr = null;
System.gc();//再次让GC工作,使得MyDemo的对象再次被回收
Thread.sleep(5000);//单位是毫秒,让当前程序休眠5秒再结束
}
}
class MyDemo{
private int value;
public MyDemo(int value) {
this.value = value;
}
@Override
public String toString() {
return "MyDemo{" + "value=" + value + '}';
}
//重写finalize方法,让大家看一下它的调用效果
@Override
protected void finalize() throws Throwable {
// 正常重写,这里是编写清理系统内存的代码
// 这里写输出语句是为了看到finalize()方法被调用的效果
System.out.println("我轻轻的走了,不带走一段代码....");
TestFinalize.add(this);
//把当前对象this放到一个数组中,这样就有变量引用它,当前对象就不能被回收了
//当下次this对象再次称为垃圾对象之后,GC就不会调用它的finalize()方法了
}
}
面试题:对finalize()的理解?
- 当对象被GC确定为要被回收的垃圾,在回收之前由GC帮你调用这个方法,不是由程序员手动调用。
- 这个方法与C语言的析构函数不同,C语言的析构函数被调用,那么对象一定被销毁,内存被回收,而finalize方法的调用不一定会销毁当前对象,因为可能在finalize()中出现了让当前对象“复活”的代码
- 每一个对象的finalize方法只会被调用一次。
- 子类可以选择重写,一般用于彻底释放一些资源对象,而且这些资源对象往往时通C/C++等代码申请的资源内存
- 重写toString、equals和hashCode方法(Alt+Ins)
建议使用IDEA中的Alt + Insert快捷键,而不是Ctrl + O快捷键。
3、标准JavaBean
JavaBean
是 Java语言编写类的一种标准规范。符合JavaBean
的类,要求:
(1)类必须是具体的和公共的,
(2)并且具有无参数的构造方法,
(3)成员变量私有化,并提供用来操作成员变量的set
和get
方法。
(4)重写toString方法
public class ClassName{
//成员变量
//构造方法
//无参构造方法【必须】
//有参构造方法【建议】
//getXxx()
//setXxx()
//其他成员方法
}
编写符合JavaBean
规范的类,以学生类为例,标准代码如下:
public class Student {
// 成员变量
private String name;
private int age;
// 构造方法
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// get/set成员方法
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
//其他成员方法列表
public String toString(){
return "姓名:" + name + ",年龄:" + age;
}
}