Java面向对象编程-中级

Java面向对象编程-中级

目录

面向对象三大特征-封装

基本介绍

在Java中,封装是指将类的内部实现细节隐藏起来,只暴露必要的接口给外部使用。通过封装,我们可以保护类的内部数据不被外部访问和修改,同时也可以对外部提供公共的方法来操作类的内部数据。

封装的好处包括:

  1. 数据隐藏:封装可以隐藏类的内部实现细节,防止外部直接访问和修改类的内部数据,保证数据的安全性。
  2. 访问控制:封装可以通过访问修饰符(public、private、protected)来控制外部对类的内部数据的访问权限,确保只有授权的程序才能访问数据。
  3. 简化代码:通过封装,我们可以将类的内部实现细节与外部接口分离开来,使得类的设计更加清晰和易于维护。
  4. 代码重用:封装可以使得类的实现细节与类的使用分离开来,从而可以在其他类中重用类的接口,提高代码的复用性。

封装的实现

  1. 将属性进行私有化。
  2. 提供一个公共的set方法(更改器),用于对属性判断并赋值。
  3. 提供一个公共的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 父类 {
}
  1. 子类就会自动拥有父类定义的属性和方法
  2. 父类又叫超类,基类。
  3. 子类又叫派生类。

使用细节

  1. 子类继承了所有的属性和方法,非私有的属性和方法可以在子类中直接访问,但是私有属性和方法不能再子类直接访问,要通过父类提供的公共方法去访问
    • 不能理解成私有的类成员不能被这个类的子类继承。
  2. 子类必须调用父类的构造器,完成父类的初始化。
    • 如果子类构造器没有显式调用父类构造器,默认情况下会调用父类的无参构造器。
    • 如果父类没有无参构造器,必须在子类构造器中显式(super)调用父类的构造器。
    • super() 和this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
  3. java 所有类都是Object 类的子类,Object 是所有类的基类。
  4. java总使用的单继承机制,可以使用接口机制补充单继承机制。
  5. 不能滥用继承,子类和父类之间必须满足is-a的逻辑关系。

继承中的内存机制

创建流程

  1. 最先加载父类,分别是Object类,然后加载Grandpa,再Father,最后Son。
    • 只加载一次,如果已经加载过,直接开始调用构造函数。
  2. 然后再分配堆空间:不同类的相同变量名不会冲突,堆中空间不同。
  3. 调用构造函数,不断通过super对父类调用,对变量进行初始化。
  4. 最后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表示直接父类的引用,用于访问父类的属性、方法、构造器。

  1. 访问父类的属性,但不能访问父类的private属性;super.属性名。
  2. 访问父类的方法,不能访问父类的private方法;uper.方法名(参数列表)。
  3. 访问父类的构造器:super(参数列表);只能放在构造器的第一句,只能出现一句。

super访问方法和属性表示从父类开始查找,会一直向上查找属性名/方法名,如果找到名字,就停止名字查找,如果没有访问权限,程序会产生编译错误。

与this关键字区别

区别点thissuper
访问属性从本类开始查找从父类开始查找
调用方法从本类开始查找从父类开始查找
调用构造器调用本类的构造器,必须放在构造器的首行调用父类的构造器,必须放在子类构造器的首行
特殊表示当前对象子类中访问父类的对象

方法重写/覆盖

基本介绍

父类与子类之间的多态性,对父类的函数进行重新定义,是运行时的多样性。如果在子类中定义和父类的某个方法的名称、返回类型、参数一样的方法,我们说该方法被重写(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

使用细节

  1. 子类方法的形参列表方法名称,要和父类的形参列表和方法名称一样。
  2. 子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类。(重写的时候,子类方法不能低于父类方法的可见性
    • 比如父类返回类型是Object,子类方法返回类型是String
      public object getInfo()
      public String getInfo()
  3. 子类方法不能缩小父类方法的访问权限,应该大于等于父类的权限。
    • 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 个必要条件:继承、重写和向上转型。

  1. 一个对象的编译类型运行类型可以不一致。
  2. 编译类型在定义对象时确定,不能改变。
  3. 运行类型随着运行变化。
  4. 编译类型是看"=“左边,运行类型看”="右边。
  5. 属性没有多态。

多态的向上转型

语法:父类类型引用名=子类对象

本质:父类的引用指向了子类的对象。

向上转型后,可以调用父类的所有方法(在编译阶段决定),不能调用子类中特有方法,但是最终运行效果看子类的具体实现(在运行阶段决定)。(注意:属性没有多态

多态的向下转型

语法:子类类型 引用名 = (子类类型) 父类类型

  1. 只能强转父类的引用,不能强转父类的对象。
  2. 要求父类的引用必须指向的是当前目标的对象。
  3. 向下转型后,可以调用子类类型中所有的成员。

属性没有多态

属性没有重写,属性的值取决于编译类型。

  1. 成员变量(member field)并不会像类方法一样被覆盖(override),overriding只作用于方法。当子类中的实例变量与父类中的实例变量拥有相同的名字时(即使数据类型不同),则这个实例变量就会从obj这个对象的引用类型中查找(当父类中没有时也不会前往子类查找)
  2. 当子类和父类中有两个相同名称的变量时,子类变量会隐藏父类变量,子类中的方法直接调用变量时会使用子类中的变量,如果想让方法中使用父类的变量必须加上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

强制类型转换

要在暂时忘记对象的静态类型之后使用对象的全部功能。

  1. 只能在继承层次结构内进行强制类型转换。
  2. 在将曹磊强制转换成子类之前,应该使用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函数两种情形:

  1. 如果是子类具有特定的相等性语义,则对称性需求强制使用getClass检查,即不是同一子类则判断为不相等。
  2. 如果由超类决定相等性语义,那么可以使用instanceof检测,这样不同的子类也可以相等。

在经理与员工的例子中,如果只需要对应的字段相等,就判定相等,这时候应该使用getClass检测。

但是如果使用ID进行检测,这个检测概念适用于所有子类,因此可以使用instanceof进行检测,而且应该将Employee.equals定义为final。

父类equals规范

编写完美的equals方法的技巧:

  1. 将显示参数命名为otherObject(Object类),并强制转换成另一个命名为other的变量。

  2. 检测this和oherObject是否相同:if (this == oherObject) return ture;

  3. 检测otherObject是否为null,如果为null,返回false。

  4. 比较this和otherObject的类。

    如果equals的语义可以在子类中改变(子类具有特定的相等性语义),使用getclass检测:

    if (getClass() != oherObject.getClass()) return false;
    ClassName other = (ClassName) otherObject;
    

    如果所有的子类都有相同的相等性语义(超类决定相等性语义),使用instanceof检测:

    if (!(otherObject instanceof ClassName other)) return false;
    
  5. 最后,根据相等性概念的要求来比较字段,使用==比较基本类型字段,使用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,而不是雇员的姓名或存储地址。

父类与子类
  1. 在子类中定义hashcode方法时,首先应该调用超类hashcode方法,并将返回结果进一步参与hash值运算。
  2. 在调用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类:

  1. 创建StringBuilder类,默认容量16。
  2. 调用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}

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值