Java面向对象编程-中级
目录
面向对象三大特征-封装
基本介绍
在Java中,封装是指将类的内部实现细节隐藏起来,只暴露必要的接口给外部使用。通过封装,我们可以保护类的内部数据不被外部访问和修改,同时也可以对外部提供公共的方法来操作类的内部数据。
封装的好处包括:
- 数据隐藏:封装可以隐藏类的内部实现细节,防止外部直接访问和修改类的内部数据,保证数据的安全性。
- 访问控制:封装可以通过访问修饰符(public、private、protected)来控制外部对类的内部数据的访问权限,确保只有授权的程序才能访问数据。
- 简化代码:通过封装,我们可以将类的内部实现细节与外部接口分离开来,使得类的设计更加清晰和易于维护。
- 代码重用:封装可以使得类的实现细节与类的使用分离开来,从而可以在其他类中重用类的接口,提高代码的复用性。
封装的实现
- 将属性进行私有化。
- 提供一个公共的set方法(更改器),用于对属性判断并赋值。
- 提供一个公共的get方法(访问器),用于获取属性的值。
可变对象引用与clone
可变对象引用:类中有公共方法可以改变类中属性的值。
java中,注意不要编写返回可变对象引用的访问器方法,例如:
class Employee{
private Date hireDay;
public Date getHireDay(){
return hireDay;
}
}
这里,Date对象是可变的,通过返回私有的可变对象,这时候,在类外部,可以通过调用这个函数,得到对象,并在类外对该属性进行修改,这样就会破坏封装性。
如果需要返回一个可变对象的引用,首先应该对它进行克隆(类似于C++中的拷贝操作),对象克隆是指存放在另一个新位置上的对象副本。
package com.hspedu;
public class changeable_object {
public static void main(String[] args) throws CloneNotSupportedException {
Employee employee = new Employee();
A a = employee.getA();// 通过返回器得到私有的可变对象
employee.print();
a.num =20; // 修改对象的值
employee.print(); // employee对象中封装的私有对象a被破坏
System.out.println("a.num的值 "+a.num + " a的hash值"+ a.hashCode());
System.out.println("=======================");
// 返回clone值
employee = new Employee();
employee.print();
A cloneA = employee.getCloneA();
cloneA.num=20;
employee.print(); // employee对象中封装的私有对象a被破坏
System.out.println("cloneA.num的值"+cloneA.num+" cloneA的hash值"+cloneA.hashCode());
}
}
class Employee{
// a是私有的 且为可变对象
private A a = new A();
// 通过返回器返回私有可变对象
public A getA() {
return a;
}
public A getCloneA() throws CloneNotSupportedException {
return a.clone();
}
public void print(){
System.out.println("employee中a.num的值 "+a.num+" employee中a的hash值"+a.hashCode());
}
}
class A implements Cloneable{
int num =10;
@Override
protected A clone() throws CloneNotSupportedException {
A a1 = null;
try{
a1 = (A)super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
return a1;
}
}
employee中a.num的值 10 employee中a的hash值460141958
employee中a.num的值 20 employee中a的hash值460141958
a.num的值 20 a的hash值460141958
=======================
employee中a.num的值 10 employee中a的hash值1163157884
employee中a.num的值 10 employee中a的hash值1163157884
cloneA.num的值20 cloneA的hash值1956725890
面向对象三大特征-继承
基本介绍
当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象处父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends类声明继承父类。
class 子类 extends 父类 {
}
- 子类就会自动拥有父类定义的属性和方法
- 父类又叫超类,基类。
- 子类又叫派生类。
使用细节
- 子类继承了所有的属性和方法,非私有的属性和方法可以在子类中直接访问,但是私有属性和方法不能再子类直接访问,要通过父类提供的公共方法去访问。
- 不能理解成私有的类成员不能被这个类的子类继承。
- 子类必须调用父类的构造器,完成父类的初始化。
- 如果子类构造器没有显式调用父类构造器,默认情况下会调用父类的无参构造器。
- 如果父类没有无参构造器,必须在子类构造器中显式(super)调用父类的构造器。
- super() 和this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器。
- java 所有类都是Object 类的子类,Object 是所有类的基类。
- java总使用的单继承机制,可以使用接口机制补充单继承机制。
- 不能滥用继承,子类和父类之间必须满足is-a的逻辑关系。
继承中的内存机制
创建流程
- 最先加载父类,分别是Object类,然后加载Grandpa,再Father,最后Son。
- 只加载一次,如果已经加载过,直接开始调用构造函数。
- 然后再分配堆空间:不同类的相同变量名不会冲突,堆中空间不同。
- 调用构造函数,不断通过super对父类调用,对变量进行初始化。
- 最后Son对象(0x11都是)返回给main中的引用。
示例:使用debug进行断点调试。
public class ExtendsTheory {
public static void main(String[] args) {
Son son = new Son();//内存的布局
//(1) 首先看子类是否有该属性
//(2) 如果子类有这个属性,并且可以访问,则返回信息
//(3) 如果子类没有这个属性,就看父类有没有这个属性(如果父类有该属性,并且可以访问,就返回信息..)
//(4) 如果父类没有就按照(3)的规则,继续找上级父类,直到Object...
System.out.println(son.name);//返回就是大头儿子
//System.out.println(son.age);//返回的就是0
System.out.println(son.getAge());//返回的就是39
System.out.println(son.hobby);//返回的就是旅游
}
}
class GrandPa { //爷类
static {
System.out.println("加载爷类");
}
public GrandPa() {
System.out.println("调用爷类构造函数");
}
String name = "大头爷爷";
String hobby = "旅游";
int age = 70;
}
class Father extends GrandPa {//父类
static {
System.out.println("加载父类");
}
public Father() {
System.out.println("调用父类的构造函数");
}
String name = "大头爸爸";
private int age = 39;
public int getAge() {
return age;
}
}
class Son extends Father { //子类
public int age=0;
static {
System.out.println("加载子类");
}
public Son() {
System.out.println("调用子类的构造函数");
}
String name = "大头儿子";
}
注意:
- 成员变量(member field)并不会像类方法一样被覆盖(override),overriding只作用于方法。当子类中的实例变量与父类中的实例变量拥有相同的名字时(即使数据类型不同),则这个实例变量就会从obj这个对象的引用类型中查找(当父类中没有时也不会前往子类查找)
super关键字
基本介绍
与this关键字类似,super表示直接父类的引用,用于访问父类的属性、方法、构造器。
- 访问父类的属性,但不能访问父类的private属性;super.属性名。
- 访问父类的方法,不能访问父类的private方法;uper.方法名(参数列表)。
- 访问父类的构造器:super(参数列表);只能放在构造器的第一句,只能出现一句。
super访问方法和属性表示从父类开始查找,会一直向上查找属性名/方法名,如果找到名字,就停止名字查找,如果没有访问权限,程序会产生编译错误。
与this关键字区别
区别点 | this | super |
---|---|---|
访问属性 | 从本类开始查找 | 从父类开始查找 |
调用方法 | 从本类开始查找 | 从父类开始查找 |
调用构造器 | 调用本类的构造器,必须放在构造器的首行 | 调用父类的构造器,必须放在子类构造器的首行 |
特殊 | 表示当前对象 | 子类中访问父类的对象 |
方法重写/覆盖
基本介绍
父类与子类之间的多态性,对父类的函数进行重新定义,是运行时的多样性。如果在子类中定义和父类的某个方法的名称、返回类型、参数一样的方法,我们说该方法被重写(Override)。在Java中,子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写,方法重写又称方法覆盖。
重写能够改变父类方法的语义。
public class demo {
public static void main(String[] args) {
B b = new B();
b.f1();
System.out.println("======================");
b.f2();
}
}
abstract class Base{
public abstract void f1();
public void f2(){
System.out.println("base f2");
}
}
class A extends Base{
@Override
public void f1() {
System.out.println("A f1");;
}
public void f2(){
super.f2();
System.out.println("A f2");
}
}
class B extends A{
@Override
public void f1() {
super.f1();
System.out.println("B f1");
}
@Override
public void f2() {
super.f2();
System.out.println("B f2");
}
}
A f1
B f1
======================
base f2
A f2
B f2
使用细节
- 子类方法的形参列表,方法名称,要和父类的形参列表和方法名称一样。
- 子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类。(重写的时候,子类方法不能低于父类方法的可见性)
- 比如父类返回类型是Object,子类方法返回类型是String
public object getInfo()
public String getInfo()
- 比如父类返回类型是Object,子类方法返回类型是String
- 子类方法不能缩小父类方法的访问权限,应该大于等于父类的权限。
- public > protected > 默认>private
重载和重写的比较
名称 | 发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 |
---|---|---|---|---|---|
重载 | 本类 | 必须一样 | 类型,个数,顺序至少有一个不同 | 无要求 | 无要求 |
重写 | 父子类 | 必须一样 | 相同 | 相同或子类 | 子类不能缩小访问范围 |
动态绑定
如果private方法、static方法、final方法或者构造器,那么编译器可以根据变量类型准确知道应该调用那个方法,就会静态绑定。
public class test {
public static void main(String[] args) {
B b = new B();
b.hi();
// A a = new B();
// a.hi();
}
}
class A{
private void hi(){
System.out.println("A hi");
}
}
class B extends A{
public void hi(){
System.out.println("B hi");
}
}
如果需要调用的方法准确地知道依赖于隐式参数(this)的实际类型,就会产生动态绑定。程序运行采用动态绑定的时候,虚拟机必须调用与所引用对象的实际类型所对应的那个方法,从运行类型向上查找签名。动态绑定有一个重要的特性:无须修改现有的代码就可以对程序进行拓展。
面向对象三大特征-多态
基本介绍
指在父类中定义的方法被子类继承之后,可以表现出不同的行为,这使得同一个方法在父类及其各个子类中具有不同的含义。
Java 实现多态有 3 个必要条件:继承、重写和向上转型。
- 一个对象的编译类型和运行类型可以不一致。
- 编译类型在定义对象时确定,不能改变。
- 运行类型随着运行变化。
- 编译类型是看"=“左边,运行类型看”="右边。
- 属性没有多态。
多态的向上转型
语法:父类类型引用名=子类对象
本质:父类的引用指向了子类的对象。
向上转型后,可以调用父类的所有方法(在编译阶段决定),不能调用子类中特有方法,但是最终运行效果看子类的具体实现(在运行阶段决定)。(注意:属性没有多态)
多态的向下转型
语法:子类类型 引用名 = (子类类型) 父类类型
- 只能强转父类的引用,不能强转父类的对象。
- 要求父类的引用必须指向的是当前目标的对象。
- 向下转型后,可以调用子类类型中所有的成员。
属性没有多态
属性没有重写,属性的值取决于编译类型。
- 成员变量(member field)并不会像类方法一样被覆盖(override),overriding只作用于方法。当子类中的实例变量与父类中的实例变量拥有相同的名字时(即使数据类型不同),则这个实例变量就会从obj这个对象的引用类型中查找(当父类中没有时也不会前往子类查找)
- 当子类和父类中有两个相同名称的变量时,子类变量会隐藏父类变量,子类中的方法直接调用变量时会使用子类中的变量,如果想让方法中使用父类的变量必须加上
super
关键词。
public class PolyDetail02 {
public static void main(String[] args) {
//属性没有重写之说!属性的值看编译类型
Base base = new Sub();//向上转型
System.out.println(base.count);// ?看编译类型 10
System.out.println(((Sub)base).count);//? 20
Sub sub = new Sub();
System.out.println(sub.count);//? 20
}
}
class Base { //父类
int count = 10;//属性
}
class Sub extends Base {//子类
int count = 20;//属性
}
InstanceOf比较操作符
判断对象的运行类型是否为XX类型 或者XX类型的子类型。
class AA {} //父类
class BB extends AA {}//子类
继承
BB bb = new BB();
System.out.println(bb instanceof BB);// true
System.out.println(bb instanceof AA);// true
多态
//aa 编译类型 AA, 运行类型是BB
//BB是AA子类
AA aa = new BB();
System.out.println(aa instanceof AA); // true
System.out.println(aa instanceof BB); // true
Object obj = new Object();
System.out.println(obj instanceof AA);//false
String str = "hello";
//System.out.println(str instanceof AA);
System.out.println(str instanceof Object);//true
强制类型转换
要在暂时忘记对象的静态类型之后使用对象的全部功能。
- 只能在继承层次结构内进行强制类型转换。
- 在将曹磊强制转换成子类之前,应该使用instanceof进行检查。
注意:通过强制类型转换来转换对象的类型通常并不是一个好主意,如果在超类中使用到强制类型转换,应该考虑超类设计是否合理,尽量少用强制类型转换和instanceof操作符。
instanceof模式匹配 java16:
if(e instanceof Manager m && m.setBonus() > 10000)... // 正确
if(e instanceof Manager m || m.setBonus() > 10000)... // 错误
double Bonus = e instanceof Manager m ? m.setBonus() : 0; // 正确
Object类
Java中每个类都是由Object类扩展出来的。
Object 类中的主要方法是:
方法名 | 作用 |
---|---|
boolean equals(Object o) | 比较两个对象是否相等。默认情况下,两个对象的引用相等。如果想要比较对象的内容,可以自己实现这个方法。 |
int hashCode() | 返回对象的哈希码。在使用集合等数据结构时,需要用到。 |
String toString() | 返回对象的字符串表示形式。每次调用println中会调用。 |
类<?> getClass | 返回此object的运行类型。 |
protected Object clone() | 创建并返回此对象的副本。 |
void notify() | 唤醒正在等待对象监视器的单个线程。 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程。 |
void wait() | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或者notifyAll方法,或者指定的时间已过。 |
void wait(long timeout) | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或者notifyAll方法,或者指定的时间已过。 |
注意:Java 8 中的 Object 类是抽象类,不能被直接实例化。
equals*
默认比较两个对象引用是否相同,不过经常需要基于状态检测对象属性的相等性。
判断属性相等性的健全写法
为了防止属性可能为null的情况,可以使用Objects.equals方法。
下面是Objects.equals()方法的源码:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
在子类中定义equals方法时,首先应该调用超类的equals,如果检测失败,对象就不可能相等。如果超类中的字段都相等,那么可以继续检测子类中的字段是否相等。
注意:对于数组类型的字段,可以使用静态的Arrays.equals方法检查相应的数组元素是否相等,对于多维数组,可以使用Arrays.deepEquals方法。
相等性测试与继承
Java语言规范要求equals方法具有以下性质:自反性、对称性、传递性、一致性。
问题:
如果一个Employee类和Manager类刚好具有相同的姓名、薪水、雇佣日期,这时候如果使用instanceof检测,会破坏对称性原则。
父类中定义equals函数两种情形:
- 如果是子类具有特定的相等性语义,则对称性需求强制使用getClass检查,即不是同一子类则判断为不相等。
- 如果由超类决定相等性语义,那么可以使用instanceof检测,这样不同的子类也可以相等。
在经理与员工的例子中,如果只需要对应的字段相等,就判定相等,这时候应该使用getClass检测。
但是如果使用ID进行检测,这个检测概念适用于所有子类,因此可以使用instanceof进行检测,而且应该将Employee.equals定义为final。
父类equals规范
编写完美的equals方法的技巧:
-
将显示参数命名为otherObject(Object类),并强制转换成另一个命名为other的变量。
-
检测this和oherObject是否相同:if (this == oherObject) return ture;
-
检测otherObject是否为null,如果为null,返回false。
-
比较this和otherObject的类。
如果equals的语义可以在子类中改变(子类具有特定的相等性语义),使用getclass检测:
if (getClass() != oherObject.getClass()) return false; ClassName other = (ClassName) otherObject;
如果所有的子类都有相同的相等性语义(超类决定相等性语义),使用instanceof检测:
if (!(otherObject instanceof ClassName other)) return false;
-
最后,根据相等性概念的要求来比较字段,使用==比较基本类型字段,使用Object.equals比较对象字段。如果所有的字段都匹配,就返回true,否则返回false。
示例:
// Employee类的equals
public boolean equals(Object object){
if(this == object) return true;
if(object == null) return false;
if(getClass() != object.getClass()) return false;
Employee obj = (Employee) object; // 向下转型
return Objects.equals(name,obj.name) && sal == obj.sal && Objects.equals(day,obj.day);
}
子类quals规范
在子类中定义equals方法时,首先应该调用超类equals,如果检测失败,那么对象就不可能相等。
// Manager类的equals
public boolean equals(Object object){
if (!super.equals(object)) return false;
Manager obj = (Manager) object; // 向下转型
return bonus == obj.bonus;
}
hashcode*
基本原理
hash code是由对象导出的一个整型值,没有规律。Object类中hashcode方法是一个native方法,两个不同的对象,hashcode值基本上不会相同,hashcode值是根据对象的存储地址得出的。
public native int hashCode();
字符串类对hashcode方法进行了重写,根据字符串内容计算得出,但是StringBuilder类没有重写hashcode方法。
// String类hashcode;部分源码
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
重写hashcode方法应该返回一个整数,应该合理地组合各个实例字段的hash值,尽量使不同对象的hash值分散开来。
public class Employee {
public int hashCode() {
return 7 * name.hashCode() + 11* new Double(salary).hashCode()+ 13 * hireDay.hashCode();
}
}
计算属性hash的健全写法
首先,最好使用null安全的方法Objects.hashCode。 如果其参数为null,这个方法会返回0,否则返回对参数调用hashCode的结果。另外,使用静态方法Double.hashCode来避免创建Double对象:
// Objects.hashcode源码
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
// Employee类hashcode方法
public int hashCode() {
return 7 * Objects.hashCode(name) + 11 * Double.hashCode(salary) + 13* Objects.hashCode(hireDay);
}
需要组合多个散列值时,可以调用Objects.hash并提供多个参数。 这个方法会对各个参数调用Objects.hashCode,并组合这些hash值。这样Employee.hashCode方法可以简单地写为:
//Objects.hash源码
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
// Employee类hashcode方法
public int hashCode() {
return Objects.hash(name, salary, hireDay);
}
Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。例如,如果用定义的Employee.equals比较雇员的ID,那么hashCode方法就需要散列ID,而不是雇员的姓名或存储地址。
父类与子类
- 在子类中定义hashcode方法时,首先应该调用超类hashcode方法,并将返回结果进一步参与hash值运算。
- 在调用Objects.hash时候,如果传入的使一个整数,这时候会自动装箱。
// Employee类的hashcode方法
public int hashcode() {
return Objects.hash(name, sal, day);
}
// Manager类的hashcode方法
public int hashcode() {
return Objects.hash(super.hashcode(), bonus);
}
toString*
在Object中还有一个重要的方法,就是toString方法,它用于返回表示对象值的字符串。绝大多数的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。
最好通过调用 getClass().getName()获得类名的字符串,不要将类名硬加到toString方法中。
随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符“+”连接起来,Java编译就会自动地调用toString方法,以便获得这个对象的字符串描述。例如:
B b = new B();
String message = "The instance is" + b;
// automatically invokes p.toString()
这里自动创建了StringBuilder类:
- 创建StringBuilder类,默认容量16。
- 调用StringBuilder append(String str),返回this。
示例
package com.hspedu.object_;
import java.util.Objects;
public class full {
public static void main(String[] args) {
manager man1 = new manager("小红", 25, 10, 1);
manager man2 = new manager("小红", 25, 10, 1);
System.out.println(man1.equals(man2));
System.out.println(man1.hashCode());
System.out.println(man2.hashCode());
System.out.println(man1);
System.out.println(man2);
}
}
class staff{
private String name;
private int age;
private double sal;
public staff(String name, int age, double sal) {
this.name = name;
this.age = age;
this.sal = sal;
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null) return false;
System.out.println(getClass());
System.out.println(obj.getClass());
if(getClass() != obj.getClass()) return false;
staff sta = (staff) obj;
return Objects.equals(name,sta.name)&&Objects.equals(age,sta.age)&&Objects.equals(sal,sta.sal);
}
@Override
public int hashCode() {
return Objects.hash(name,age,sal);
}
@Override
public String toString() {
return "staff{" +
"name='" + name + '\'' +
", age=" + age +
", sal=" + sal +
'}';
}
}
class manager extends staff{
private double bonus;
public manager(String name, int age, double sal, double bonus) {
super(name, age, sal);
this.bonus = bonus;
}
@Override
public boolean equals(Object obj){
if(!(super.equals(obj)))
return false;
manager sta = (manager) obj;
return Objects.equals(bonus,sta.bonus);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(),bonus);
}
@Override
public String toString() {
return super.toString()+ " manager{" +
"bonus=" + bonus +
'}';
}
}
class com.hspedu.object_.manager
class com.hspedu.object_.manager
true
1328585672
1328585672
staff{name='小红', age=25, sal=10.0} manager{bonus=1.0}
staff{name='小红', age=25, sal=10.0} manager{bonus=1.0}