Java基础知识查漏补缺(Java核心技术)----继承

Object:所有类的超类

知识点1:基本类型的数组也扩展了object类

在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布 尔类型的值都不是对象。
所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
在这里插入图片描述

知识点2:equals方法

判断两个对象是否等价,是OOP编程中常见的需求(下面围绕Java来进行阐述)。

考虑这样几种情况:通过某个特征值来判断两个对象是否“等价”,当这两个对象等价时,判断结果为true,否则结果为false。

当然,这里的“特征值”不会只是简单的“对象引用”,事实上,Object类(Java的“对象世界”的根)中实现的equals方法,就是把“特征值”设定为“对象引用”来进行判断等价性的,因此可以得知,Object类中equals方法只是简简单单地返回this引用和被判断的obj的引用的“==运算”的值。

但是很多情况下,并不是要求两个对象只有引用相同时(此时二者为一个对象)才“判定为等价”,这就需要ADT设计者来界定两个实例对象判断等价的条件,即设定要比较的特征值。

例如,如果两个雇员对象的姓名、薪水和雇佣日期都一样,就认为它们是相等的
那么具体的实现过程如下:
在这里插入图片描述
getClass方法将返回一个对象所属的类,在检测中,只有在两个对象属于同一个类时,才有可能相等。

注意:为了防备name或hireDay可能为null的情况,需要使用Objects.equals 方法。如果两个参数都为null,Objects.equals(a,b)调用将返回true;如果 其中一个参数为null,则返回false;否则,如果两个参数都不为null,则调用 a.equals(b)。利用这个方法,Employee.equals方法的最后一条语句要改写为:

在这里插入图片描述
在子类中定义equals方法时,首先调用超类的equals。如果检测失败,对象就不可 能相等。如果超类中的域都相等,就需要比较子类中的实例域。
在这里插入图片描述

知识点3:hasCode方法

散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果x和 y是两个不同的对象,x.hashCode()与y.hashCode()基本上不会相同。

在表5中列出了几个通过调用String类的hashCode方法得到的散列码。
在这里插入图片描述
String类使用下列算法计算散列码:
在这里插入图片描述
由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值 为对象的存储地址。来看下面这个例子。
在这里插入图片描述
表5-2列出了结果。
在这里插入图片描述
请注意,字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出 的。而字符串缓冲sb与tb却有着不同的散列码,这是因为在StringBuffer类中没有 定义hashCode方法,它的散列码是由Object类的默认hashCode方法导出的对象存储地址。

如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插 入到散列表中。

hashCode方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散 列码,以便能够让各个不同的对象产生的散列码更加均匀。

例如,下面是Employee类的hashCode方法。
在这里插入图片描述
不过,还可以做得更好。首先,最好使用null安全的方法Objects.hashCode。如 果其参数为null,这个方法会返回0,否则返回对参数调用hashCode的结果。

另外,使用静态方法Double.hashCode来避免创建Double对象:
在这里插入图片描述
还有更好的做法,需要组合多个散列值时,可以调用Objects.hash并提供多个参 数。这个方法会对各个参数调用Objects.hashCode,并组合这些散列值。这样 Employee.hashCode方法可以简单地写为:
在这里插入图片描述
Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么 x.hashCode()就必须与y.hashCode()具有相同的值。例如,如果用定义的
Employee.equals比较雇员的ID,那么hashCode方法就需要散列ID,而不是雇员 的姓名或存储地址。

提示:如果存在数组类型的域,那么可以使用静态的Arrays.hashCode方法计 算一个散列码,这个散列码由数组元素的散列码组成。

知识点4: toString方法

只要对象与一个字符串通过操作符“+”连 接起来,Java编译就会自动地调用toString方法,以便获得这个对象的字符串描述
在调用x.toString()的地方可以用""+x替代。这条语句将一个空串与 x的字符串表示相连接。这里的x就是x.toString()。与toString不同的是,如 果x是基本类型,这条语句照样能够执行。

提示:
令人烦恼的是,数组继承了object类的toString方法,数组类型将按照 旧的格式打印。例如:

在这里插入图片描述
生成字符串“[I@1a46e30”(前缀[I表明是一个整型数组)。修正的方式是调用静 态方法Arrays.toString。代码:
在这里插入图片描述
将生成字符串“[2,3,5,7,11,13]”。
要想打印多维数组(即,数组的数组)则需要调用Arrays.deepToString方法

建议:
强烈建议为自定义的每一个类增加toString方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅

代码清单

下面的程序清单的程序实现了Employee类和Manager类的equals、hashCode和toString方法。

EqualsTest.java

public class EqualsTest {
    
    
    public static void main(String[] args) {
    
    
        Employee alice1=new Employee("Alice Adams",75000,1987,12,15);
        Employee alice2=alice1;
        Employee alice3=new Employee("Alice Adams",75000,1987,12,15);
        Employee bob=new Employee("Bob Brandson",50000,1989,10,1);

        System.out.println("alice1==alice2: "+(alice1==alice2));  //true  引用地址相同
        System.out.println("alice1==alice3:"+(alice1==alice3));   //false   引用地址不同
        System.out.println("alice1.equals(alice3): "+alice1.equals(alice3));  // true 域值都相同
        System.out.println("alice1.equals(bob): "+alice1.equals(bob));  //false 域值不相同
        System.out.println("bob.toString(): "+bob); //Employee name='Bob Brandson', salary=50000.0, hireDay=1989-10-01}

        Manager carl=new Manager("Carl Cracker",80000,1987,12,15);
        Manager boss=new Manager("Carl Cracker",80000,1987,12,15);
        boss.setBonus(5000);
        System.out.println("boss.toString(): "+boss); //Manager{bonus=5000.0} Manager name='Carl Cracker', salary=80000.0, hireDay=1987-12-15}
        System.out.println("Carl.equals(boss): "+carl.equals(boss)); //false 奖金bonus不同
        System.out.println("alice1.hashCode(): "+alice1.hashCode()); //-808853550
        System.out.println("alice3.hashCode(): "+alice3.hashCode()); //-808853550
        System.out.println("bob.hashCode: "+bob.hashCode()); //-624019882
        System.out.println("carl.hashCode(): "+carl.hashCode());  //-2004699436
    }
}

Employee.java

import java.time.LocalDate;
import java.util.Objects;

public class Employee {
    
    
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String name, double salary, int year, int month, int day){
    
    
        this.name=name;
        this.salary=salary;
        this.hireDay=LocalDate.of(year,month,day);
    }

    public String getName() {
    
    
        return name;
    }

    public double getSalary() {
    
    
        return salary;
    }

    public LocalDate getHireDay() {
    
    
        return hireDay;
    }

    public void raiseSalary(double byPercent){
    
    
        double raise=salary*byPercent/100;
        salary+=raise;
    }

    @Override
    public boolean equals(Object o) {
    
    
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return Double.compare(employee.salary, salary) == 0 &&
                Objects.equals(name, employee.name) &&
                Objects.equals(hireDay, employee.hireDay);
    }

    @Override
    public int hashCode() {
    
    
        return Objects.hash(name, salary, hireDay);
    }

    @Override
    public String toString() {
    
    
        return getClass().getName()+
                " name='" + name + '\'' +
                ", salary=" + salary +
                ", hireDay=" + hireDay +
                '}';
    }
}

Manager.java

import java.util.Objects;

public class Manager extends Employee {
    
    
    private double bonus;

    public Manager(String name,double salary,int year,int month,int day){
    
    
        super(name,salary,year,month,day);
        bonus=0;
    }

    public double getSalary(){
    
    
        double baseSalary=super.getSalary();
        return baseSalary+bonus;
    }

    public void setBonus(double bonus) {
    
    
        this.bonus = bonus;
    }

    @Override
    public boolean equals(Object o) {
    
    
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false;
        Manager manager = (Manager) o;
        return Double.compare(manager.bonus, bonus) == 0;
    }

    @Override
    public int hashCode() {
    
    
        return Objects.hash(super.hashCode(), bonus);
    }

    @Override
    public String toString() {
    
    
        return "Manager{" +
                "bonus=" + bonus +
                "} " + super.toString();
    }
}

对象包装器与自动装箱

有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的 类。例如,Integer类对应基本类型int。通常,这些类称为包装器(wrapper)。 这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、 Short、Byte、Character、Void和Boolean(前6个类派生于公共的超类 Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在 其中的值。同时,对象包装器类还是final,因此不能定义它们的子类

特点:

ArrayList<Integer>  list=new ArrayList<>();
list.add(3);

这里面list.add(3)将自动变成

list.add(Integer.valueOf(3))

这种变换被称为自动装箱(autoboxing)

科普:注释:大家可能认为自动打包(autowrapping)更加合适,而“装箱 (boxing)”这个词源自于C#。

相反地,当将一个Integer对象赋给一个int值时,将会自动地拆箱。也就是说,编 译器将下列语句:

int n=list.get(i);

翻译成

int i=list.get(i).intValue();

甚至在算术表达式中也能够自动地装箱和拆箱。例如,可以将自增操作符应用于一 个包装器引用:
在这里插入图片描述
编译器将自动地插入一条对象拆箱的指令,然后进行自增计算,最后再将结果装 箱。

大多数情况下,容易有一种假象,即基本类型与它们的对象包装器是一样的,只是 它们的相等性不同。大家知道,==运算符也可以应用于对象包装器对象,只不过检 测的是对象是否指向同一个存储区域,因此,下面的比较通常不会成立:
在这里插入图片描述
然而,Java实现却有可能(may)让它成立。如果将经常出现的值包装到同一个对 象中,这种比较就有可能成立。这种不确定的结果并不是我们所希望的。解决这个 问题的办法是在两个包装器对象比较时调用equals方法

注释:
自动装箱规范要求boolean、byte、char≤127,介于-128~127之间的 short和int被包装到固定的对象中。例如,如果在前面的例子中将a和b初始化为 100,对它们进行比较的结果一定成立。

关于自动装箱还有几点需要说明。首先,由于包装器类引用可以为null,所以自动 装箱有可能会抛出一个NullPointerException异常:
在这里插入图片描述
另外,如果在一个条件表达式中混合使用Integer和Double类型,Integer值就会 拆箱,提升为double,再装箱为Double:
在这里插入图片描述
最后强调一下,装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字 节码时,插入必要的方法调用。虚拟机只是执行这些字节码。

使用数值对象包装器还有另外一个好处。Java设计者发现,可以将某些基本方法放置在包装器中,例如,将一个数字字符串转换成数值。
要想将字符串转换成整型,可以使用下面这条语句:
在这里插入图片描述
这里与Integer对象没有任何关系,parseInt是一个静态方法。但Integer类是放
置这个方法的一个好地方。

警告:
有些人认为包装器类可以用来实现修改数值参数的方法,然而这是错误 的。在第4章中曾经讲到,由于Java方法都是值传递,所以不可能编写一个下面这样 的能够增加整型参数值的Java方法。

public static void triple(int x){
    
        //won't work
       x=3*x;   //modifies local variables
}

将int替换成Integer又会怎样呢?

public static void triple(Integer x){
    
      //won't work
     
}

问题是Integer对象是不可变的:包含在包装器中的内容不会改变。不能使用这些 包装器类创建修改数值参数的方法。

如果想编写一个修改数值参数值的方法,就需要使用在org.omg.CORBA包中定义的 持有者(holder)类型,包括IntHolder、BooleanHolder等。每个持有者类型都 包含一个公有(!)域值,通过它可以访问存储在其中的值。

public static void triple(IntHolder x){
    
    
      x.value=3*x.value;
}

参数数量可变的方法

在Java SE 5.0以前的版本中,每个Java方法都有固定数量的参数。然而,现在的 版本提供了可以用可变的参数数量调用的方法(有时称为“变参”方法)。

前面已经看到过这样的方法:printf。例如,下面的方法调用:

System.out.printf("%d",n);
System.out.printf("%d %s",n,"daniel");

在上面两条语句中,尽管一个调用包含两个参数,另一个调用包含三个参数,但它们调用的都是同一个方法。
printf方法是这样定义的:
在这里插入图片描述
这里的省略号…是Java代码的一部分,它表明这个方法可以接收任意数量的对象 (除fmt参数之外)。

实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组, 其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值,自 动装箱功能将把它们转换成对象)。现在将扫描fmt字符串,并将第i个格式说明符 与args[i]的值匹配起来。

换句话说,对于printf的实现者来说,Object…参数类型与Object[]完全一样。

编译器需要对printf的每次调用进行转换,以便将参数绑定到数组上,并在必要的 时候进行自动装箱:比如这一句

System.out.printf("%d %s",n,"daniel");

装箱成

System.out.printf("%d %s",new object[]{
    
    new Integer(n),"daniel"});

用户自己也可以定义可变参数的方法,并将参数指定为任意类型,甚至是基本类 型。下面是一个简单的示例:其功能为计算若干个数值的最大值。

public static double max(double... values){
    
    
        double largest=Double.NEGATIVE_INFINITY;
        for(double x:values){
    
    
            if(x>largest)
                largest=x;
        }
        return largest;
    }

编译器将new double[]{3.1,40.4,–5}传递给max方法。
甚至可以将main方法声明为下列形式:

public static void main(String... args)

枚举类

详细见这篇文章叙述:https://www.cnblogs.com/alter888/p/9163612.html

import java.util.Scanner;

public class EnumTest {
    
    
    public static void main(String[] args) {
    
    
        Scanner in=new Scanner(System.in);
        System.out.println("Enter a size:(SMALL MEDIUM,LARGE,EXTRA_LARGE)");
        String input=in.next().toUpperCase();
        Size size=Enum.valueOf(Size.class,input);
        System.out.println("size="+size);
        System.out.println("abbreviation="+size.getAbbreviation());
        if(size==Size.EXTRA_LARGE)
            System.out.println("Good job");
    }
}

enum Size{
    
    
    SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
    private String abbreviation;
    public String getAbbreviation(){
    
    return abbreviation;}
    private Size(String abbreviation){
    
    this.abbreviation=abbreviation;}
}

继承的设计技巧

  • 1.将公共操作和域放在超类
    这就是为什么将姓名域放在Person类中,而没有将它放在Employee和Student类中 的原因。

  • 2.不要使用受保护的域
    有些程序员认为,将大多数的实例域定义为protected是一个不错的主意,只有这 样,子类才能够在需要的时候直接访问它们。然而,protected机制并不能够带来 更好的保护,其原因主要有两点。第一,子类集合是无限制的,任何一个人都能够 由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了 封装性。第二,在Java程序设计语言中,在同一个包中的所有类都可以访问 proteced域,而不管它是否为这个类的子类。
    不过,protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法 很有用。

  • 3.使用继承实现“is-a”关系

  • 4.除非所有继承的方法都有意义,否则不要使用继承

  • 5.在覆盖方法时,不要改变预期的行为

  • 6.使用多态,而非类型信息

  • 7.不要过多地使用反射

猜你喜欢

转载自blog.csdn.net/weixin_42684418/article/details/104246586