继承

  1. Java中不支持多重继承即一个类有两个父类,在C++中支持
  2. C++中,如果希望支持动态绑定,需要将成员函数声明为virtual。在Java中不存在虚函数,动态绑定是默认的行为
  3. Javaprotected的安全性要差于C++
  4. 强烈建议对每一个自定义的类重写toString方法!!!!

类、超类(父类)和子类

定义子类

使用关键字extends表示继承,能够从父类中革新出新的功能。

public class Manager extends Employee {
    // added methods add fields
}

覆盖方法(override)

父类中有些方法对子类并不适用。例如在Employee类中,getSalary方法返回值为薪资,而在Manager类中,该方法应该返回薪资和奖金之和。

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

当子类需要调用父类被重写的方法时,需要使用super关键字;在这里若不使用super关键字,那么会调用自身的getSalary方法而导致程序崩溃

子类构造器

  1. Java语言中,每个类都至少有一个构造器。若程序中没有显示定义任何构造器,则Java程序将提供一个隐式的构造器。只要程序中显式定义了构造器,则不会提供隐式的默认构造器
  2. 若子类中含有默认无参构造方法(这时候会默认调用父类的),父类中没有,则会报错。故必须在父类中显式的写出无参构造器!!!
  3. 若子类调用父类中的构造器,则需在在构造器里加入super方法:
public class Employee {
    private String name;
    private double salary;
    private int year, month, day;
    
    public Employee() {
    }
    
    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        this.year = year;
        this.month = month;
        this.day = day;
    }
}
public class Manager() extends Employee {
    public Manager() {
        super();//若注释掉这一行,且注释调6,7行,则会报错;
    }
    
    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);// 调用带有name、salary、year、month、day参数的构造器
                                              // 若注释掉这一行,则该构造函数会调用父类中的public Employee() {}
    }
}
  1. 若父类声明了无参构造器和有参构造器,则super关键字可以不被使用,因为子类会默认调用父类的无参构造器
  2. 构造器的调用顺序:父类->子类

继承层次

可以通过Manager类派生出Executive类,并且一直下去。

一个祖先类可以有多个子孙类

多态

在Java程序设计语言中,对象变量是多态的。

多态的存在的三个必要条件

  1. 继承
  2. 重写
  3. 向上转型:声明一个父类对象变量,引用一个子类对象,主要适用于仅需父类的功能就能完成的
Employee e = new Manager();//所有经理一定是员工,但员工不一定是经理

向下转型:强制类型转换,主要为了使用子类中的新特性

Manager s = (Manager)e;

理解方法调用(好难!!!!)

  1. 编译器查看对象的声明类型和方法名。
  2. 接下来,编译器要确定方法调用中提供的参数类型。
  3. 确定访问权限:如果是private、static、final,编译器能够准确地得到调用的方法,这被称为静态绑定;若调用的方法依赖于隐式参数的实际类型,那么被称为动态绑定
  4. 程序运行并且采用动态绑定调用方法时,JVM虚拟机预先为每个类计算一个方法表,调用时虚拟机会查询这一张表

阻止继承:final类和方法

定义一个子类不允许拓展

public final class Executive extends Manager {

}

当把一个类声明为final时,其中的方法自动地使用final但不包括字段

再说强制类型转换

基本数据类型之间的强制类型转换按过不表;在继承中使用强制类型转换的唯一原因是:使用对象的新特性

  • 只能在继承层次内进行强制类型转换
  • 在将父类强制转换成子类之前,应该使用instanceof进行检查
if (e instanceof Manager)

抽象类

自下而上看,更上层的类更具有一般性,那么最上层的祖先类应该提取了子孙类的共性;本质上设计时是不会创建该类的对象的,该类是抽象的 ;抽象的目的是为了概括(解释)这些具体的事物

使用关键字abstract

public abstract class Person {
    public abstract void eat();
}

抽象类不能实例化,即不能创建对象,即Person Tom = new Person();是非法的!

但可以定义一个抽象类的对象变量去引用非抽象子类创建的对象:Person Jerry = new Employee();

从现实意义去考虑,Person作为一个整体,它的一些特性也应是抽象的(即概括了的,比如每个人喜欢吃的食物是不同的),故在抽象类中应该(不是必须)用抽象方法:public abstract void eat();。**抽象方法没有函数体!!!且包含一个或者多个抽象方法的类本身必须被声明为抽象的 **

对于一个抽象类的子类,必须重写父类中的抽象方法:

public class Student extends Person {
    public void eat() {
        System.out.println("在食堂吃!");
    }
}

受保护访问

对于希望子类访问父类中的某个字段,可以将该字段设置成protected

总结一下访问控制修饰符

private

仅对本类可见

public

对外部可见

protected

对本包和所有子类可见

default

仅对本包可见(默认),其实没有default关键字

Object

Object类是所有类的祖先类

1. equals方法

Object类中的equals方法用来检测一个对象是否等于另一个对象

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

自定义equals方法

public class Employee {
    public boolean equals(Object otherObject) {
        if (this == otherObject) return true;
        if (otherObject == null) return false;
        if (getClass != otherObject.getClass()) return false;
        
        Employee other = (Employee) otherObject;
        
        return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay);
    }
}

Java语言规范要求equals方法具有以下特性:

  • 自反性:对于任何非空引用x,x.equals(x)应该返回true
  • 对称性:对于任何引用xy,当且仅当y.equals(x)返回true时,x.equals(y)返回true
  • 传递性:对于任何引用x,yz,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)也返回true
  • 一般性:如果x和y引用的对象没有发生变化,则x.equals(y)应该返回相同的结果
  • 对于任意非空引用x,x.equals(null)应该返回false

如何编写一个完美的equals方法:

  1. 显式参数命名为otherObject,稍后需要将它强制转换成另一个名为other的变量。
  2. 检测thisotherObject是否相等:if (this == otherObject) return true;
  3. 检测otherObject是否为null,如果为null,返回falseif (otherObject == null) reutrn false;
  4. 比较this与otherObject的类。如果equals的语义可以在子类中改变,就使用getClass检测
  5. otherObject强制转换成相应类类型的变量:Classname other = (Classname) otherObject;
  6. 现在根据相等性概念来比较字段。使用==比较基本类型字段,使用Objects.equals比较对象字段。如果所有字段都匹配,返回true;否则返回falsereturn field1 == other.field1 && Objects.equals(field2, other.field2)如果在子类中重新定义equals,就需要使用super.equals(other)调用

2. hashCode方法

hash code 是由对象导出的一个整型值。hash code 没有规律。

java.lang.Object

**int hashCode():**返回对象的散列码,两个相等的对象要求返回相等的散列码

3. toString方法

它会返回表示对象值的一个字符串。

绝大多数toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的字段值。

public String toString() {
    return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]";
}

子类中的toString方法

public class Manager extends Employee {
    public String toString() {
        return super.toString() + "[bonus=" + bonus + "]";
    }
}

如果x是任意对象,并调用了System.out.println(x);println方法会调用x.toString()方法,打印并输出得到的字符串

注意数组继承了Object类中的toString方法,故无法显示数组;解决办法是使用Arrays.toString()方法

ArrayList

主要是为了解决动态数组的问题

1. 声明数组列表

需要使用一对尖括号将类名括起来追加到ArrayList后面:

ArrayList<Employee> staff = new ArrayList<Employee>(100);// 初始容量为100

使用add方法将元素添加到数组列表中。若容量100不够,ArrayList会自动创建一个更大的数组,将小的数组拷贝到更大的数组里

staff.add(new Employee("Harry Hacker",...));

使用size方法将返回数组列表中包含的实际元素个数。

staff.size();

2. 访问数组列表元素

我们不能直接使用中括号[]来访问数组列表元素,而要使用getset方法

staff.set(i, harry); // 相当于给staff[i]赋值
Employee client = (Employee) staff.get(i);

如何将一个数组列表转换为数组

Employee[] e = new Employee[staff.size()];
staff.toArray(e);

在数组列表中插入元素和删除元素

int i = staff.size() / 2;
staff.add(i,e);// 第一个参数是插入的位置,第二个参数是数组列表名
Employee e = staff.remove(i);

3. 类型化于原始数组列表的兼容性

**Java5**之前没有类型数组列表,而有一个原始数组列表:保存Object类型元素的ArrayList

出于兼容性的考虑,编译器检查到无错误后,就将所有类型化的数组列表转换成原始ArrayList对象。在程序运行时,所有的数组列表中没有类型参数,即JVM虚拟机中没有类型参数。

4. 对象包装器与自动装箱

有时候,需要将int这样的基本类型转换成对象,所有的基本类型都有一个与之对应的类,这样的类称为包装器(Interger, Long, Float, Double, Short, Byte, Character, Boolean)前六个类的公共父类是Number

包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值;同时,包装器类还是final,即不能派生子类。

可以看出包装器类能够很好的解决数组列表不能使用基本数据类型的问题:ArrayList<Integer> array = new ArrayList<Integer>();

添加元素:array.add(3);// 将整数3转换成了Interger类的对象

实际调用的是array.add(Integer.valueOf(3));

这样的转换被称为自动装箱

相反地,将一个Integer对象赋给一个int值将会自动拆箱

int n = array.get(i);

int n = array.get(i).intValue();

需要强调的是装箱和拆箱是编译器所做的工作而不是JVM虚拟机

Integer类

int intValue()// 将这个Integer对象的值作为一个int返回

static String toString(int i)// 返回一个新的String对象,表示指定数值i的十进制表示

static int parseInt(String s)// 返回字符串s表示的十进制整数

static Integer valueOf(String s)// 返回一个新的Integer对象,用字符串s表示的十进制整数初始化

参数数量可变的方法

printf方法就是变参方法

使用方法:在基本类型后加一个省略号(三个点),但可变参数必须是最后一个参数

public static void test(int... numbers) {
    for (int i = 0; i < numbers.length; i++) {
        System.out.print(numbers[i] + " ");
    }
}

我们可以看到,可变参数的实质是数组

Enum

public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE}// 定义了四个实例

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

而且 Java 要求必须先定义 enum 实例。

枚举的构造器总是私有的。

所有枚举类型都是Enum的子类,继承了Enum类的方法,其中toString方法返回枚举常量名

Size.SMALL.toString()// 返回字符串"SMALL"
Size s = Enum.valueOf(Size,"SMALL");// 将s设置为Size.SMALL
  1. static Enum valueOf(Class enumClass, String name)返回指定类中有指定名字的枚举常量
  2. String toString()返回枚举常量名
  3. int ordinal()返回枚举常量在enum声明中的位置,从0开始
  4. int compareTo(E other)如果枚举常量出现在other之前,返回一个负整数;若this == other,则返回0;否则返回一个正整数

反射

反射库(reflection library)提供了一个丰富且精巧的工具集,可以用来编写能够动态操作Java代码的程序。

能够分析类能力的程序称为反射。

  • 在运行时分析类的能力
  • 在运行时检查对象。例如,编写一个适用于所有类的toString方法。
  • 实现泛型数组操作代码。
  • 利用Method对象

注解(Annotation)

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。

格式:以“@注释名”在代码中存在

内置注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

作用在代码的注解是

  • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。

元注解:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)

从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

自定义注解

import java.lang.annotation.*;

@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Arithmetic {
    int firstNumber();
    int secondNumber();
}

Class

加载完类之后,在方法区中就产生了一个Class类型的对象,***JVM中只能有一个对象实例!!!***。这个对象就包含了完整的类的结构信息,我们可以通过这个对象看到类的结构。

获取类名

Employee e;
Class cd = e.getClass();
System.out.println(cd.getNzme() + "" + e.getName());

也可以通过静态方法forName来获得class对象

Class.forName("这里输入类名");

特殊的一个获取对象方法

每一个基本数据类型的包装类都有一个*Type*属性

Class cl = Integer.Type

类的初始化(更多内容参见《深入理解JVM》)

当类主动加载时才会发生类的初始化!!!

  1. 主动加载
  • 当虚拟机启动时,先初始化main方法所在的类
  • new一个类的对象
  • 调用类的静态成员和静态方法
  • 使用java.lang.reflect包的方法对类进行反射调用
  • 当初始化一个类时,如果父类没有被初始化,则先会初始化它的父类
  1. 被动加载
  • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化
  • 通过数组定义类引用,不会触发此类的初始化
  • 引用常量不会触发此类的初始化(常量在链接阶段就存入运行时常量池了)

类加载器

  • 作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后再堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
  • 类缓存

分类:引导类加载器、扩展类加载器,系统类加载器

// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

// 获取系统类加载器的父类加载器-->扩展类加载器
ClassLoader parentClassLoader = systemClassLoader.getParent();

// 获取扩展类加载器的父类加载器-->根加载器,java获取不到这个加载器,返回null
ClassLoader rootClassLoader = parentClassLoader.getParent();

利用反射分析类的能力

  1. 获取类名:getSimpleName()
  2. 获取类的所有public属性:getFileds()//返回一个String数组
  3. 获取类的所有属性:getDeclaredFileds()
  4. 获取类的指定的公有属性:getFiled(String str)
  5. 获取类的指定的非继承属性:getDeclaredFiled(String str)
  6. 获取类和父类的所有public方法:getMethods()
  7. 获取类的所有构造器:getConstructors()

使用反射在运行时分析对象

setAccessible(boolean flag)方法通过将flag值设置为true来关闭类加载器检测以提升效率

使用反射来操作泛型

  • ParameterizedType:表示一种参数化类型,比如Collection<String>
  • GenericArrayType:表示一种元素类型时从参数化类型或者类型变量的数组类型
Method method = UserMap.class.getMethod("mapTest", List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
for (Type genericParameterType : genericParameterTypes) {
    System.out.println(genericParameterType);
    if (genericParameterType instanceof ParameterizedType) {
        Type[] actualType = ((ParameterizedType)genericParameterType).getActualTypeArguments();
        for (Type type : actualType) {
            System.out.println(type);
        }
    }
}

继承的设计技巧

  1. 将公共操作和字段放在父类
  2. 不要使用受保护的字段(protected),但protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用
  3. 使用继承实现“is-a”关系
  4. 除非所有继承的方法都有意义,否则不要使用继承(父类的有些方法,可能子类用不上)
  5. 在覆盖方法时,不要改变预期的行为。
  6. 使用多态,而不要使用类型信息。???
  7. 不要滥用反射