10.继承、重写与多态

本章目标

  • 继承
  • 重写
  • super关键字
  • 向上转型
  • Object类
  • String补充

本章内容

员工类中有一个方法working,销售人员类属性于员工的一种也有working方法,那么还需要在销售人员类中再定义一个working方法吗

一、继承

1、什么是继承

继承是一种特性,利用继承可以重用现有类生成新类,也是代码重用的一种体现。

  • 通过关键字extends继承一个已有的类
  • 父类:被继承的类(超类,基类)
  • 子类:新的类(派生类)
  • Java继承的特点:单继承,只有一个直接父类
  • 继承可以持续进行,形成复杂的继承层级结构
  • 如果一个类的声明中没有使用关键字extends,那么这个类被系统默认为是继承了Object父类

动物都有个功能eat,那么我们在动物类Animal中定义eat方法,当子类Dog,Person需要的时候,直接继承就好了,不需要在对这部分进行再次定义,效率就大大提高了。

2、继承的作用

继承简单总结有以下作用:

  • 继承简化了人们对事物的认识和描述,能清晰体现相关类间的层次结构关系。
  • 提供软件复用功能。
  • 通过增强一致性来减少模块间的接口和界面,大大增加程序的易维护性。

3、继承特性

父类中的:

  • public成员:将被子类继承、直接使用;
  • private成员:将被隐藏,在子类中无法访问;
  • default成员:可以被在同一个包中的子类继承、访问,在不同包中的对子类隐藏;
  • protected 的成员:子类的成员方法可以直接访问到父类的protected成员,不论子类和父类是否在同一个包中

4、示例

  • 父类

    package com.it.hrms;
     public class Employee {
      private String empId;
         ……
      public void working(){
        System.out.println(“working”);
      }
     }
    
  • 子类

    package com.it.hrms;
     public class Salesman extends Employee {
         public void saleProduct () {
             System.out.println(“sale the product”);
        }
     }
    

二、重写

1、什么叫重写

方法重写是指,子类中定义了一个方法,并且这个方法的名字、返回类型、参数类型及参数的个数与从父类继承的方法完全相同。

注:变量的类型必须相同,但名字可以不同

2、重写目的及作用

子类可以通过方法重写来隐藏继承父类的方法,又称为“覆盖”。

  • 通过方法重写,子类可以把父类的状态和行为变成自己的状态和行为。
  • 只要父类的方法能够被子类继承,子类就能重写这个方法。
  • 一旦子类重写了这个方法,就表示隐藏了所继承的这个方法。
  • 如果通过子类对象调用这个方法,那也是调用重写后的方法。

3、示例

Salesman重写了父类的工作方法,改为销售产品

 public class Salesman extends Employee {
 @Override
 public void working() {
     System.out.println("sale the product");
  }
 }

4、多态性

多态性是指允许不同类的对象对同一消息做出响应。多态性包括参数化多态性和包含多态性。

多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序方法同名问题。

与继承有关的多态性是指父类的某个方法被其子类重写时,可以各自产生自己的功能行为,指同一个操作被不同类型对象调用时产生不同的行为。

多态性体现在两方面:

  • 重载——发生在同一个类中多态
  • 重写(覆盖)——发生在子类中

示例3中,如果在子类中希望调用被隐藏的父类方法,这该怎么办?必须使用关键字super,这就引出了我们下一个课题——super关键字

三、super关键字

1、应用场景

  • super的使用场合用来访问直接父类隐藏的数据成员,其使用形式如下:

      super.数据成员
    
  • 用来调用直接父类中被覆盖的成员方法,其使用形式如下:

      super.成员方法(参数)
    
  • 用来调用直接父类的构造方法,其使用形式如下:

       super(参数)
    

2、调用被父类重写的方法

通过super在子类中调用被隐藏的父类的成员变量和方法

 public class Salesman extends Employee {
     @Override
     public void working() {
         super.working();
     }
 
 }

3、调用父类构造方法

super调用父类构造方法

  1. 子类不继承父类的构造方法
  2. 子类在创建对象时,子类的构造方法总是调用父类的某个构造方法。
  3. 如果父类有多个构造方法,那么子类默认调用的是那个不带参数的构造方法。
  4. 如果父类只有一个带参的构造方法,那么子类必须在自己的构造方法中用super语句来调用父类的带参的构造方法,否则程序会报错。
  5. 如果子类的构造方法中没有写super语句,那么系统将默认有“super();”存在,即调用父类的不带参的构造方法。

3.1、默认调用父类不带参的构造方法

 public class Salesman extends Employee {
     public SaleMan() {
         super();
     }
     @Override
     public void working() {
         super.working();
         //System.out.println("sale the product");
     }
 
 }

3.2、父类声明带参的

每个类都有一个默认不带参的构造方法,当声明了带参的构造方法之后 ,默认构造方法将失效,或者显示声明

 public class Employee {
     private String empId;
     private String empName;
     private float salary;
 
     public Employee(String empId, String empName, float salary) {
         super();
         this.empId = empId;
         this.empName = empName;
         this.salary = salary;
     }
     ……
 }

此时再查看子类,会发现报错了

 The constructor Employee() is undefined

3.3、方案一调用父类带参的构造方法

 public class Salesman extends Employee {
     public SaleMan(String empId, String empName, float salary) {
         super(empId, empName, salary);
     }
     @Override
     public void working() {
         super.working();
         //System.out.println("sale the product");
     }
 
 }

3.4、方案二父类显示无参构造方法

public class Employee {
    private String empId;
    private String empName;
    private float salary;
    public Employee() {
        // TODO Auto-generated constructor stub
    }

    public Employee(String empId, String empName, float salary) {
        super();
        this.empId = empId;
        this.empName = empName;
        this.salary = salary;
    }
    ……
}

4、创建子类对象过程(了解)

子类对象会拥有父类定义的所有成员变量,但是,他不一定有父类成员变量的访问权限。可以想象为,子类对象里面套了一个父类的对象,但是本质上堆区只有一个子类对象。更多参考

当创建一个子类对象时,整个过程如下:

  1. 分配内存:

    Java为子类对象分配内存,这块内存包括子类的所有成员以及从父类继承的成员。

  2. 父类初始化:

    调用父类构造方法(通过、super()),初始化父类的成员。并不是创建了一个单独的父类对象而是初始化子类对象中属于父类的部分。

  3. 子类初始化: 在父类的构造方法执行完后,返回到子类构造方法继续初始化子类类的成员。

四、向上转型

1、向上转型

向上转型 – 父类的引用变量可以指向子类的对象 如:

 Employee emp = new Salesman(…);

2、特性

引用变量emp由于是按照Employee类型声明的,因此:

  • 只能调用父类的成员方法或公共属性
  • Salesman类型特有的方法和属性则不能使用
  • 子类重写父类方法,调用的是重写后的方法

3、示例

public class Test {

    public static void main(String[] args) {
        Employee emp = new SalesMan();
        emp.working();
    }

}

五、Object类

1、简介

Object类是所有类的超类,也就是说,Java中的每一个类都是由Object类扩展而来的。因而每当你创建一个对象,它都将拥有Object类中的全部方法

2、常用方法

方法 用途
Object clone() 创建与该对象的类相同的新对象。
boolean equals(Object) 比较两对象是否相等。
void finalize() 当垃圾回收器确定不存在对该对象的引用时,垃圾回收器在对该对象执行垃圾回收前调用该方法。
class getClass() 返回一个对象的运行时类型信息。
int hashCode() 返回该对象的散列码值。
void notify() 激活等待在该对象的监视器上的一个线程。
void notifyAll() 激活等待在该对象的监视器上的全部线程。
String toString() 返回该对象的字符串表示。
void wait() 等待这个对象另一个更改线程的通知。
void wait(long) 等待这个对象另一个更改线程的通知。
void wait(long, int) 等待这个对象另一个更改线程的通知。

3、重写equals方法

Object类所提供的只是一些基本的方法,我们在编写自己的类时经常需要覆盖这些方法,一方面是加强功能,另一方面也是为了适应当前的情况。

Object类中的equals方法用来判断两个对象是否相等,这个方法需要在子类中根据不同的情况各自实现(重写)。最常见的就是用它来比较两个字符串是否相等。标准的java类库中有超过150个equals方法的实现。

 public boolean equals(Object obj) {
     return (this == obj);
 }

3.1、String字符串比较之equals方法和==的区别

String string1 = "aaa";
String string2 = "aaa";
String string3 = new String("aaa");
String string4 = new String("aaa");
String1 == string2;      //返回true
String1.equals(string2);//返回true
String3 == string4;      //返回false
String3.equals(string4);//返回true

首先string1="aaa";和string2="aaa"; 都指向常量池的同一个对象aaa; 其调用==和X. equals(Y)方法其效果是一样的

问:什么是常量池呢?我们可以先了解一下常量池,再看equals的源码

String string3 = new String("aaa");String string4 = new String("aaa");是在heap堆中创建两个新对象,他们引用的地址是不同的,从而使得==出现不相等的情况。

而X. equals(Y)当x和Y所引用的对象是同一类对象且属性内容相等(并不一定是相同对象)时返回true,就出现了上面的结果

3.2、常量池(了解)

Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到常量池中。

Java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean。这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 两种浮点数类型的包装类Float,Double并没有实现常量池技术

字符串池是Java为了重用String对象而设置的一个缓存池,字符串池的实现有一个前提条件:String对象是不可变的。因为这样可以保证多个引用可以同时指向字符串池中的同一个对象。如果字符串是可变的,那么一个引用操作改变了对象的值,对其他引用会有影响,这样显然是不合理的

更多参考:https://www.cnblogs.com/xiaotian15/p/6971353.html

3.3、Integer类型比较

两个Integer类型的元素比较会是什么效果呢?

       Integer a = 127;
        Integer b = 127;
        System.out.println(a==b);
        Integer c = 128;
        Integer d = 128;
        System.out.println(c==d);

源码分析:

 /**
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    /**
     * The value of the {@code Integer}.
     *
     * @serial
     */
    private final int value;

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

3.4、重写规范

java语言规范要求equals方法具有以下性质:

  • 自反性:对于任何非空引用x,x.equals(x)返回true。
  • 对称性:对于任何非空引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)返回true。
  • 传递性:对于任何引用x,y和z,如果x.equals(y)返回true并且y.equals(z)也返回true,那么x.equals(z)应该返回true。
  • 一致性:如果x和y引用的对象没有改变,那么x.equals(y)的重复调用应该返回同一结果。
  • 对于任何非空引用x,x.equals(null)应该返回false。

3.5、示例

在比较两个emp对象时,比较的是两个对象的地址,如果两个对象的值相同,我们也认为相同,这时怎么处理

public class Test {

    public static void main(String[] args) {
        Employee emp = new Employee("1001", "tom", 8000f);
        Employee emp2 = new Employee("1001", "tom", 8000f);
        System.out.println(emp.equals(emp2));
    }

}

结果

false

3.6、重写示例

一般重写equals和重写hashcode同步进行,可以一起重写后再演示

@Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Employee other = (Employee) obj;
        if (empId == null) {
            if (other.empId != null)
                return false;
        } else if (!empId.equals(other.empId))
            return false;
        if (empName == null) {
            if (other.empName != null)
                return false;
        } else if (!empName.equals(other.empName))
            return false;
        if (Float.floatToIntBits(salary) != Float.floatToIntBits(other.salary))
            return false;
        return true;
    }

4、重写hashCode

hashCode是jdk根据对象的地址或者字符或者数字算出来的int类型的数值。支持此方法是为了提高哈希表的性能(例如:java.util.Hashtable提供的哈希表)

public native int hashCode();

扩展:

已知散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。那么所以hashcode()作用就是提高效率。 当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?(注意:集合中不允许重复的元素存在) 也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值

4.1、hashCode的通用规定:

  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode方法都必须始终返回同一个值。在一个应用程序与另一个应用程序的执行过程中,执行hashCode方法所返回的值可以不一致。
  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中的hashCode方法都必须产生同样的整数结果
  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的hashCode方法,则不一定要求hashCode方法必须产生不用的结果,但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能,

由上面三条规定可知,如果重写了equals方法而没有重写hashCode方法的话,就违反了第二条规定,相等的对象必须拥有相等的hash code。

4.2、示例

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((empId == null) ? 0 : empId.hashCode());
        result = prime * result + ((empName == null) ? 0 : empName.hashCode());
        result = prime * result + Float.floatToIntBits(salary);
        return result;
    }

4.3、运行结果

再次比较两个对象,结果返回true

5、重写toString方法

Object 类提供的toString方法总是返回该对象实现类的类名 + @ +hashCode值

另一个经常需要重写的方法是

public String toString()

这个方法用来返回字符串形式的对象信息。例如当执行“System.out.println(obj);”时,系统将自动调用obj对象的toString方法,并将返回值输出

@Override
    public String toString() {
        return "Employee [empId=" + empId + ", empName=" + empName + ", salary=" + salary + "]";
    }

6、instanceof说明(了解)

instanceof 是一个 Java 关键字,用于检查一个对象是否是一个类的实例或者是其子类的实例。它的语法是 对象 instanceof 类名,返回一个布尔值。

例如,我们可以使用 instanceof 来判断一个对象是否是一个特定类的实例:

public class Animal {
    // ...
}

public class Cat extends Animal {
    // ...
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Cat();

        if (animal instanceof Cat) {
            System.out.println("animal 是 Cat 的实例");
        }
    }
}

在上面的例子中,Cat 是 Animal 的子类。通过使用 instanceof 关键字,我们可以判断 animal 是否是 Cat 类的实例。

六、字符串补充

1、final关键字

1.1、final 修饰符

final 修饰符可应用于类、方法和变量,在应用于类、方法和变量时意义是不同的,但本质是一样的:final表示不可改变。

1.2、final 类

final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final 的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类

final 类示例:java.lang.String

示例

public final class TestFinal{
   private int i = 7;
   private int j = 1;
   public void foo() {
        ……
    }
}
//无法继承,因为父类为final类型
class Further extends TestFinal {
    ……
}

1.3、final 方法

在方法声明中使用 final 关键字向编译器表明子类不能覆盖此方法

在声明类中,一个 final 方法只被实现一次如果一个类为 final 类,那么它的所有方法都为隐式的

final 方法保证了程序的安全性和正确性

class TestFinal{
  final void f(){
    ……
  }
}

class Further extends TestFinal{
  //无法重写
  final void f(){
    ……
  }
}

2、字符串常量

字符串是final修饰的,不能被继承

字符串是常量,是不可变的:private final char value[];

    public static void main(String[] args) {
        String str = "abc";
        testString(str);
        System.out.println(str);
    }
    public static void testString(String str) {
        str = str+"de";
    }

以上代码String str = "abc"会在常量池创建一个常量abc,

str = str+"de";jdk1.8之后字符串拼接底层会自动生成一个 StringBuilder()对象,并把新对象地址赋值给str,所以str地址已经改变

3、StringBuffer类、 StringBuilder

StringBuffer是可变的,因为底层采用 char[] value;,没有加final,所以可变,可以看其父类AbstractStringBuiler中有一个字符数组类型的属性,和String不同的是,这个数组没有被final修饰,所以是可变的

由于String的内容不可以变化,所以在拼接、替换字符串内容时实际上是在新建一个新的字符串常量,如果需要在一个字符串上经常进行拼接、替换等操作,那么效率会很低。

在这种场合下,可以使用StringBuffer类来拼接、插入或是删除字符串中的字符序列,直到完成所有操作后再使用StringBuffer对象的toString方法来得到一个完成的字符串。

从JDK5.0开始,Java类库中又新引入了StringBuilder类来完成同样的功能,与StringBuffer相比提供的方法一样,效率更高一点,但是在多线程环境下工作是还是需要使用StringBuffer类。

3.1、字符串拼接

public class Main {
    public static void main(String[] args) {
        String str = "hello";
        str = str + "world";
        System.out.println(str);
    }

}

CFR反编译如下:

java -jar cfr-0.152.jar D:\workspace\idea\day0914\target\classes\com\woniuxy\Main.class --stringbuilder false
/*
 * Decompiled with CFR 0.152.
 */
package com.woniuxy;

public class Main {
    public static void main(String[] args) {
        String str = "hello";
        str = new StringBuilder().append(str).append("world").toString();
        System.out.println(str);
    }
}

3.2、构造方法

构造方法 说明
StringBuffer() 构造一个没有字符的字符串缓冲区,初始容量为16个字符
StringBuffer(CharSequence seq) 构造一个包含与指定的相同字符的字符串缓冲区 CharSequence
StringBuffer(int capacity) 构造一个没有字符的字符串缓冲区和指定的初始容量。
StringBuffer(String str) 构造一个初始化为指定字符串内容的字符串缓冲区。

3.3、常用方法

方法 说明
append(String str) 将指定的字符串附加到此字符序列。
capacity() 返回当前容量。
deleteCharAt(int index) 删除 char在这个序列中的指定位置。
insert(int offset, char c) 在此序列中插入 char参数的字符串表示形式。

3.4、示例

package com.it;

public class BuilderApp
{
  public static void main(String[ ] args)
  {
      Object x="hello";
        String s="good bye";
        char cc[ ]={'a','b','c','d','e','f'};
        boolean b=false;
        char c='Z';
        long k=12345678;
        int i=7;
        float f=2.5f;
        double d=33.777;
        StringBuffer buf=new StringBuffer( );
        buf.append(x); buf.append(' '); buf.append(s);
        buf.append(' '); buf.append(cc); buf.append(' ');
        buf.append(cc,0,3); buf.append(' ');buf.append(b);
        buf.append(' '); buf.append(c); buf.append(' ');
        buf.append(i); buf.append(' '); buf.append(k);
        buf.append(' '); buf.append(f); buf.append(' ');
        buf.append(d);
        System.out.println("buf="+buf);
      }
}

插入操作

package com.it;

public class BuilderInsert {
    public static void main(String[ ] args)
      {
        Object y="hello";
        String s="good bye";
        char cc[ ]={'a','b','c','d','e','f'};
        boolean b=false;
        char c='Z';
        long k=12345678;
        int i=7;
        float f=2.5f;
        double d=33.777;
        StringBuffer buf=new StringBuffer( );
        buf.insert(0,y); buf.insert(0,' '); buf.insert(0,s);
        buf.insert(0, ' '); buf.insert(0,cc); buf.insert(0,' ');
        buf.insert(0,b); buf.insert(0,' '); buf.insert(0,c);
        buf.insert(0, ' ');
        buf.insert(0,i); buf.insert(0,' ');
        buf.insert(0,k); buf.insert(0,' ');
        buf.insert(0,f);
        buf.insert(0,' '); buf.insert(0,d);
        System.out.println("buf="+buf);
      }
}

3.5、String、StringBuffer、StringBuilder的区别:

String StringBuffer StringBuilder
执行速度 最差 其次 最高
线程安全 线程安全 线程安全 线程不安全
使用场景 少量字符串操作 多线程环境下的大量操作 单线程环境下的大量操作

3.6、StringBuffer扩容(扩展)

在进行字符串append添加的时候,会先计算添加后字符串大小,传入一个方法:ensureCapacityInternal 这个方法进行是否扩容的判断,需要扩容就调用newCapacity方法进行扩容:

private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

尝试将新容量扩为原大小的2倍+2(加2是因为拼接字符串通常末尾都会有个多余的字符),如果扩充后的容量还是不够,则直接扩充到需要的容量大小

思维导图

image

posted @ 2025-03-31 17:43  icui4cu  阅读(14)  评论(0)    收藏  举报