201871010124王生涛第六七周JAVA学习总结

项目

内容

这个作业属于哪个课程

https://www.cnblogs.com/nwnu-daizh/

这个作业的要求在哪里

https://edu.cnblogs.com/campus/xbsf/2018CST1/homework/8690

作业学习目标

  1. 深入理解程序设计中算法与程序的关系;
  2. 深入理解java程序设计中类与对象的关系;
  3. 理解OO程序设计的第2个特征:继承、多态;
  4. 学会采用继承定义类设计程序(重点、难点);
  5. 能够分析与设计至少包含3个自定义类的程序;
  6. 掌握利用父类定义子类的语法规则及对象使用要求。

随笔博文正文内容包括:

第一部分:总结第五章理论知识

第5章 继承

5.1 类、超类、子类

5.1.1 定义子类

不同于C++的冒号继承法,关键字extends表示继承。

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 b)
    {
        bonus=b;
    }
}
注:java中所有的继承都是公有继承。
子类比超类封装了更多的数据,子类拥有超类的所有域和方法,除此之外,上面的例子中Manager就比超类Employee多了bonus这个数据域,和setBonus这一方法。Manager有自己的构造器,但Manager类不能访问超类的私有域。构造器传递参数给超类构造器用来初始化超类的数据域,要使用关键字super。
而C++中使用冒号带上超类的构造函数来构造超类私有域的。

5.1.2.覆盖方法

因为经理的工资肯定和员工的工资不一样,所以在getSalary上也不能用一致的方法,容易发现上述例子中Manager类提供了一个和超类同名的getSalary方法,叫做覆盖。
注:Manager类的方法不能直接访问超类私有域,具体方法详见super的解释,已经整理
所以在覆盖方法中必须要得到基础工资,不可避免要访问超类的salary,就要用super关键字去调用超类的方法访问超类的私有域。

5.1.3.多态

Employee类对象变量可以引用Employee对象,也可以引用Manager类对象。但是反过来,Manager类对象变量并不能引用Employee对象。道理很简单,通俗的讲,Manager一定是Employee,但是Employee一定是Manager吗?不一定吧。
而从类内部的数据角度分析,因为子类一定有更多的方法或者数据域,而如果用子类对象变量去引用一个超类对象,超类对象并不存在那些子类多出来的方法或数据域,这明显是有问题的引用。但反过来,超类对象变量去引用子类对象时候,当把对象看做超类,并不存在这个变量不能调用的方法。
多态:一个对象可以指示多种实际类型的现象称为多态。
如下:

Manager boss = new Manager(...);

Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee(...);
staff[2] = new Employee(...);
上述例子中 staff是一个Employee类的数组,但是staff[0]引用了一个Manager类的boss对象,而staff[1]和staff[2]则引用了Employee对象。这就是典型的多态。
与多态相关的概念还有动态绑定。
动态绑定:在运行时,上述对象变量能自动地选择调用哪个方法的现象称为动态绑定。
如下:

for(Employee e:staff)
    system.out.println(e.getName()+" "+e.getSalary());
在这个例子里面,注意到getSalary是被覆盖了的,那么e.getSalary()会调用超类还子类的方法呢?答案是根据其实际类型来自动选择。java虚拟机知道e引用对象的实际类型,因而能够正确调用。

重点关注:重载和覆盖的区别

覆盖是子类的方法与超类方法同名,同参数列表即同签名,当时用超类对象变量引用子类对象时,调用被覆盖的方法,尽管是超类的对象变量,但是调用的方法是子类中被覆盖的方法。在覆盖时,对应方法在子类的可见性必须不低于超类的可见性。
重载仅仅同名,但是参数列表不同。
注意:

- 超类对象变量可以引用子类对象,但是不能使用超类对象变量去调用子类方法,下面这样是不合法的:

staff[0].setBonus(1000);
只有

boss.setBonus(1000);
但是同时注意,由于动态绑定,对于覆盖的方法,使用超类对象变量去调用,是合法的。这看上去和上述概念是矛盾的,但可以觉得是偷梁换柱“暗中”调用了子类方法。
- 子类数组的引用可以转换成超类数组的引用,而不需要强制类型转换。也就是:

Manager[] managers = new Manager[10];
Employee[] staff = managers;
这样做是合法的,相当于staff数组的每一个Employee对象变量都引用了一个Manager对象。但是回忆一下数组的引用,需要注意的问题是,managers和staff引用的是同一个数组!而比较恐怖的是,且看如下操作:

staff[0] = new Employee(...);
编译器接受了这句操作(这看上去是不可思议的,因为managers和staff引用的是同一个数组,等于说是managers[0]=new Employee(...),但编译器这种情况下就是接受了)由于managers和staff引用同一个数组,当用managers[0].setBonus(1000)时会出现错误,因为那个实际的对象根本就不是Manager类,从而不存在setBonus这个方法,这是一个有趣的问题。一定要避免这样的事情发生,所以所有数组都要牢记创建他们的元素类型。


5.1.4.理解方法调用


另外还要注意,static方法不存在继承和覆写,它与声明时候的类相关,假设有Shape类,Cirlce类继承了Shape,这两个类里面各自声明了一个static void draw()方法,当有Shape s = new Circle(),s.draw()调用的不是Circle的draw方法,而Circle c = new Circle(),c.draw()才会调用Circle的draw方法。

5.2 object:所有类的超类

Object 类是Java中所有类的始祖,在Java中每个类都是由它扩展来的。

可以使用Object类型的变量引用任何类型的对象。

所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。

1、equals方法

  在Object类中,equals方法用于判断两个对象是否具有相同的引用。Object类中源码如下:  

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

  如果两个对象具有相同的引用,它们一定是相等的。但是对于大多数类来说,这种判断没有什么意义,因为经常会需要判断两个对象的状态是否相等,状态相等就认为相等。下面给出编写一个完美equals方法的建议:  

  1)显示参数命名为 otherObject

  2)检测 this 和 otherObject 是否引用同一个对象:if  ( this == otherObject ) return true; 起到优化的作用

  3)检测 otherObject 是否为 null,如果为 null,返回 false:if (otherObject == null) return false;

  4)比较 this 与 otherObject 是否属于同一个类,如果 equals 的语义在每个子类中有所改变,即由子类的属性状态决定相等性,就使用 getClass 检测:if (getClass() != otherObject.getClass()) return false;如果所有的子类都拥有统一的语义,即由超类决定相等的概念,那么就使用 instanceof 检测:if (!(otherObject instanceof ClassName)) return false;

  5)将 otherObject 转换成相应的类类型变量:ClassName other = (ClassName) otherObject

  6)现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。 return field1 == other.field1 && Objects.equals(field2,other.field2) && ...;如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。


  提示:对于数组类型的域,可以使用Arrays.equals方法检测相应的数据元素是否相等。

顺便说一下:

  在Java中,instanceof 运算符的前一个操作符是一个引用变量,该变量为空则返回 false,后一个操作数通常是一个类(可以是接口),用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是返回 true,否则返回 false 。也就是说在使用 instanceof 关键字做判断时, instanceof  操作符的左右操作数必须有继承或实现关系。

 

使用 Objects 类的 equals 方法 Objects.equals(field2,other.field2) 避免如果 field2 为 null 时,field2.equals(other.field2) 这种比较方法会产生异常,Objects 类在 java.util 包中,源码如下:

    public static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); } 

  举个栗子:

 View Code 

运行结果:

1
2
empOne equals empTwo:false
empOne equals empTwo:true

子类中定义equals方法:

 View Code

到这里基本说完了,还有Java语言规范要求的 equals 方法的 5 条特性,可以在API文档中 Object 类中 equals 方法查看。

 

2、hashCode方法

  散列码是由对象导出的一个整形值。Object类中定义的hashCode方法源码如下:

1
public native int hashCode();

  使用了native关键字,我看不懂具体方法实现,有时间看完 native关键字 这篇博客应该能明白一些,具体以后再说,暂时不常用。

  String 类使用下列算法计算散列码:

1
2
3
int hash = 0;
for (int i = 0; i < length(); i++)
    hash = 31 * hash + charAt(i);

  由于 hashCode 方法定义在 Object 类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。 使用 Object 类的这个方法可以用 Objects 的 hashCode 方法保证 null 安全。

  如果重新定义 equals 方法,就必须重新定义 hashCode 方法,以便用户可以将对象插入到散列表中(散列表在核心技术I第9章讨论)。

  hashCode方法应该返回一个整形数值,并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。例如,下面是 Employee 类的 hashCode 方法。

1
2
3
4
@Override   //组合多个散列值,如果存在数组类型的域,使用静态的Arrays.hashCode方法计算一个散列码,equals与hashCode的定义必须一致
public int hashCode() {
    return Objects.hash(name, salary, hireDay);
}

  涉及的源码如下:

 View Code

  equals 与 hashCode 的定义必须一致:如果 x.equals(y)  返回 true,那么 x.hashCode() 就必须与 y.hashCode() 具有相同的值。例如,如果用定义的 Employee.equals 比较雇员的ID,那么 hashCode 方法就需要散列ID,而不是雇员的姓名或存储地址。

  先说到这吧,后面看了散列表再理解。

 

3、toString方法

   Object 中的一个重要方法,用于返回表示对象值的字符串。

  下面是 Employee 类中的 toString 方法的实现:

1
2
3
4
5
6
7
8
@Override
public String toString() {
    return getClass().getName()
            "[name=" + name
            ",salary=" + salary
            ",hireDay=" + hireDay
            "]";
}

  下面是 Manager 类中的 toString 方法:

1
2
3
4
@Override
public String toString() {
    return super.toString() + "[bonus=" + bonus + "]";
}

  随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符 “+” 连接起来,Java 编译就会自动地调用 toString 方法,以便获得这个对象的字符串描述。

    提示:在调用 x.toString() 的地方可以用 ""+x 替代。这里的 x 就是 x.toString() 。与 toString 不同的是,如果 x 是基本类型,这条语句照样能够执行。

  System.out.println(x); println 方法就会直接调用 x.toString(x) ,并打印输出得到的字符串。

  Object 类定义了 toString 方法,用来打印输出对象所属的类名和散列码。例如 System.out.println(System.out); 语句输出 java.io.PrintStream@154617c ,因为 PrintStream 类没有覆盖 toString 方法。Object 类的toString方法源码如下:

1
2
3
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

    警告:令人烦恼的是,数组继承了 object 类的 toString 方法,数组类型将按照旧的格式打印,生成字符串如 “[I@1a46...”(前缀 [I 表明是一个整形数组)。修正的方式是调用静态方法 Arrays.toString,多维数组调用 Arrays.deepToString 方法。

  toString 方法是一种非常有用的调试工具。在标准类库中,许多类都定义了 toString方法, 以便用户能够获得一些有关对象状态的必要信息。强烈建议为自定义的每一个类增加 toString 方法。

 到这里基本把 Object 类的重要方法介绍了,后续有新东西会继续添加。

 

 程序清单: 

 

 Employee.java
 Manager.java

运行结果:

empOne equals empTwo:false
empOne equals empTwo:true
-1893166707
-1893166707
com.song.Employee[name=song,salary=10.0,hireDay=2019-05-25]
com.song.Employee[name=song,salary=10.0,hireDay=2019-05-25]
com.song.Employee[name=song,salary=10.0,hireDay=2019-05-25]
empOne equals managerOne:false
managerOne equals managerTwo:false
managerOne equals managerThree:true
-1893166707
-1683903746
com.song.Manager[name=song,salary=10.0,hireDay=2019-05-25][bonus=0.0]
com.song.Manager[name=song,salary=10.0,hireDay=2019-05-25][bonus=0.0]
com.song.Manager[name=wang,salary=100.0,hireDay=2019-05-28][bonus=0.0]
java.io.PrintStream@154617c

Process finished with exit code 0
复制代码
复制代码

  至于结果是两个不同类的对象生成的散列码是相同的这个情况,是否违背了上面说的 equals 与 hashCode 的定义必须一致的原则。

5.3 泛型数组列表

java.util.ArrayList<T>

ArrayList<>()  构造一个空数组列表

ArrayList<T>(int initialCapacity)  用指定容量构造一个空数组列表

boolean add(T obj)  在数组列表的尾端添加一个元素

int size() 返回存储在数组列表中的当前元素数量。(这个值小于或等于数组列表的容量)

void ensureCapacity(int capacity)  确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素。

void trimToSize()  将数组列表的存储容量消减到当前尺寸

 

使用set()和get()方法实现访问或改变数组元素的操作,而不使用[]

例如,要设置第i个元素,可以使用:

staff.set(i, harry);

等价于对数组a的元素赋值(数组的下标从0开始):

a[i] = harry;

注意:

只有i小于或等于数组列表的大小时,才能够调用list.set(i,x)。例如,下面这段代码是错误的:

ArrayList<Employee> list = new ArrayList<>(100); //capacity 100,size 0

list.set(0,x); // no element 0 yet

使用add方法为数组添加新元素,而不要使用set方法, 它只能替换数组中已经存在的元素内容。

Employee e = staff.get(i);

等价于:

Employee e = a[i];

下面这个既可以灵活扩展数组(对小型数组),又可以方便地访问数组元素。

首先,创建一个数组,并添加所有的元素

ArrayList<X> list = new ArrayList<>();

while(...)

{

  x = ...;

  list.add(x);

}

执行完之后,使用toArray方法将数组元素拷贝到另一个数组中

X[] a = new [list.size()];

list.toArray(x);

5.4  对象包装类与自动装箱

5.4.1 对象型包装类
对象型(Object的子类):Boolean、Character(char)

5.4.2 装箱与拆箱
装箱:将基本数据类型变为包装类对象,通过每个包装类的构造方法实现装箱处理
拆箱:将包装类中包装的基本数据类型取出,利用的是××Value()方法

eg:Integer提供的intValue()
public class Test {
public static void main(String[] args) {
//装箱
Integer integer = new Integer(10);
//拆箱
int data = integer.intValue();
System.out.println(data+10);
}
}

5.4.3 自动拆装箱(语法糖)
JDK1.5新特性

public class Test {
public static void main(String[] args) {
//自动装箱 将基本数据类型变成对象
Integer integer = 10;
//自动拆箱
System.out.println(integer+10);
}
}

5.5 参数数量可变的方法

5.5.1,概述

在java SE 5.0 之前版本,每个java方法都是固定参数的。然而,现在提供了可变参数的方法调用。

5.5.2,定义

举例:

public class PrintStream{

    ……

    ​public PrintStream printf(String fmt,Object . . . args){ //三个英文句号 表示多参数参数

     ​    ​return format(fmt,args);

     }

5.5.3 使用

System.out.print("d% %s",n,"hello");

 实际上 args 等价于args[] ,所以在程序中当作数据类型处理。在这个例子中参数类型是任意的。也可以是某个类型的。比如

//多参数入参

public  int max(int... ints){

int maxi=0;

if(ints.length>0){

maxi=ints[0];

for(int i:ints){

if(i>maxi){maxi=i;}

}

}

System.out.println(maxi);

return maxi;

}

5.6 枚举类

5.6.1 定义

从JDK1.5之后,出现了枚举类这么一个概念,就是使用enum关键字来定义枚举类,枚举类enum是引用数据类型,可以简单地理解为一种特殊的java类。

枚举类的对象个数是有限的且固定的,可以将每一个对象一一列举出来。

5.6.2.枚举类的继承、被继承、实现接口问题

枚举类可以实现其他接口,但是不能继承其他的类,因为所有枚举类在编译后的字节码中都继承自 java.lang.Enum,因为Java没有多继承,所以不能继承其他的类;

枚举类也不能被其他类继承,因为所有枚举类在编译后的字节码中都是继承自 java.lang.Enum(由编译器添加)的 final class 类,final 的类是不允许被派生继承的。

5.6.3.Java枚举如何保证线程安全

因为 Java 类加载与初始化是 JVM 保证线程安全,而 Java enum 枚举在编译器编译后的字节码实质是一个 final 类,每个枚举类型是这个 final 类中的一个静态常量属性,其属性初始化是在该 final 类的 static 块中进行,而 static 的常量属性和代码块都是在类加载时初始化完成的,所以自然就是 JVM 保证了并发安全。

5.7 反射

5.7.1  使用反射的三种方式

 {  1, 类名.class

     2.对象.getClass()

     3.使用Class.forName("路径+类名")}

5.7.2 定义

在程序运行状态中,对于任意一个类,都能够知道它的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

通俗点讲:有一个类,我想访问这个类中的成员变量、构造方法和成员方法,正常情况下,我们拿到的是这个类的.java文件,那么我直接通过new关键字创建一个类的实例对象,然后通过这个实例对象就可以访问这个类的成员变量、构造方法、成员方法了。但是现在我没有这个类的.java文件,而是只能拿到了这个类的.class文件,那么如何去访问到这个类中的成员变量、构造方法和成员方法呢?这就是反射机制解决的问题了。

第二部分:实验部分

1、实验目的与要求

(1) 理解继承的定义;

(2) 掌握子类的定义要求

(3) 掌握多态性的概念及用法;

(4) 掌握抽象类的定义及用途。

2、实验内容和步骤

实验1:测试程序1

Ÿ 在elipse IDE中编辑、调试、运行程序5-1 —5-3(教材152页-153页) ;

Ÿ 掌握子类的定义及用法;

Ÿ 结合程序运行结果,理解并总结OO风格程序构造特点,理解Employee和Manager类的关系子类的用途,并在代码中添加注释;

Ÿ 删除程序中Manager类、ManagerTest类,背录删除类的程序代码,在代码录入中理解父类与子类的关系和使用特点。

例题5.1程序代码如下:

复制代码
package inheritance;

/**
 * This program demonstrates inheritance.
 * @version 1.21 2004-02-21
 * @author Cay Horstmann
 */
public class ManagerTest
{
   public static void main(String[] args)
   {
      // construct a Manager object
      var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
      boss.setBonus(5000);//创建一个新经理,并设置他的奖金,setBonus属于manager的特有方法

      var staff = new Employee[3];//定义一个包含三个雇员的数组

      // fill the staff array with Manager and Employee objects

      staff[0] = boss;
      staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
      staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
      //父类引用子类对象;并将经理和雇员放入数组中
      // print out information about all Employee objects
      for (Employee e : staff)
         System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
      //输出每个人的姓名及薪水
   }
}
复制代码

输出截图如下:

 例题5.2程序代码如下:

复制代码
package inheritance;

import java.time.*;

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;
      hireDay = LocalDate.of(year, month, day);
   }
   //域访问器 
   public String getName()//取得name这个属性的值
   {
      return name;
   }

   public double getSalary()//取得Salary这个属性的值
   {
      return salary;
   }

   public LocalDate getHireDay()
   {
      return hireDay;
   }

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

输出截图如下:

例题5.3程序代码如下:

复制代码
package inheritance;

public class Manager extends Employee
{
   private double bonus;

   /**
    * @param name the employee's name
    * @param salary the salary
    * @param year the hire year
    * @param month the hire month
    * @param day the hire day
    */
 //构造器
   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 b)
   {
      bonus = b;
   }
}

实验总结:

大致明白了什么是类、子类、超类,掌握了父类与子类的部分用法,学会了如何定义抽象类,使用super关键字等。初步的理解了继承的结构层次和多态性的概念。了解了子类和父类的关系。也学到了很多有用的知识,

posted @ 2019-10-07 19:29  201871010124-王生涛  阅读(201)  评论(1编辑  收藏  举报