文章目录

  • java核心技术笔记
  • 5 继承
  • 5.2 Object: 所有类的超类
  • 5.3 泛型数组列表 ArrayList<>
  • 5.4 对象包装器与自动装箱
  • 5.5 参数数量可变的方法
  • 5.6 枚举类
  • 5.7 反射
  • 5.8 继承的设计技巧
  • 6 接口、lambda表达式与内部类
  • 6.1 接口
  • 6.2 接口示例
  • 6.3 lambda表达式


java核心技术笔记

5 继承

C++注释:C++用冒号,且有私有继承、公有继承和保护继承。Java中只有公有继承。

public class Manager extends Employee{
	// 添加额外的方法和域
}

子类 extends 超类(父类 基类)
子类功能更多。

覆盖方法:
使用同样签名的函数覆盖超类方法。
使用超类的指针(实际上比喻不恰当,super并不是一个对象的引用,而是指示编译器调用超类方法的特殊关键字):super.xxx()。

C++注释:Employee::getSalary() 相当于 Java的super.getSalary()

子类构造器:
可使用super(...)调用超类的构造器。
由于Manager 类的构造器不能访问Employee 类的私有域, 所以必须利用Employee 类的构造器对这部分私有域进行初始化, 我们可以通过super 实现对超类构造器的调用。使用super 调用构造器的语句必须是子类构造器的第一条语句

如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java 编译器将报告错误。

原则:构造子类时,一定要调用合适的超类构造器,编译器会有一些默认的行为。

this可以隐式参数,可以调其他构造器。super可以调超类方法,也可以调超类构造器。

函数重载:多态根据实际对象类型调用相应的方法!

C++注释:Java中不需要将方法声明为virtual方法,动态绑定是默认的处理方式。如果不希望一个方法有虚拟特征,就标记为final。

多态:
“is-a”规则的另一种表述法是置换法则。表明程序中出现超类的地方,都可以用子类对象置换。

为了确保不发生子类数组转超类数组带来的问题, 所有数组都要牢记创建它们的元素类型, 并负责监督仅将类型兼容的引用存储到数组中。

理解方法调用:非常重要!!

  1. 找候选函数名。(public,且名字相同)
  2. 找候选函数签名,可能会有类型转换存在,较为复杂。

!允许子类将覆盖方法的返回类型定义为原返回类型的子类型。
。例如, 假设
Employee 类有

public Employee getBuddy() { . . . } // 超类返回超类
public Manager getBudd() { . . . } // OK to change return type
// 可协变的返回类型

如果是private, static, final的方法,那么编译器准确知道调用哪个方法。这种叫static binding,静态绑定。

当程序运行时,且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。
虚拟机为每个类生成方法表。方法表中列出了这个类定义的所有方法。

Employee:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)

Manager:
getName()-> Employee.getName() // 继承
getSalary() -> Manager.getSalary() // 重写
getHireDay() -> Employee.getHireDay() // 继承
raiseSalary(double) -> Employee.raiseSalary(double) // 继承
setBonus(double) -> Manager.setBonus(double) // 新增

!! 在覆盖方法时,子类方法不能低于超类方法的可见性。超类public,子类一定要声明public,否则编译器将会把它解释为试图提供更严格的访问权限。

阻止继承:final类和方法。

// final修饰class,子类不能继承
public final class Executive extends Manager{
	...
}
public class Employee{
	public final String getName(){
		return name; // final修饰函数,禁止子类覆盖
	}
}

final修饰域则表示构造完了就不允许修改他们的值了。

将方法或类声明为final主要目的是: 确保它们不会在子类中改变语义。

有些程序员认为: **除非有足够的理由使用多态性, 应该将所有的方法都声明为final。**事实上,在C++ 和C# 中, 如果没有特别地说明, 所有的方法都不具有多态性。我们提倡在设计类层次时,仔细地思考应该将哪些方法和类声明为final

强制类型转换:

Manager boss = (Manager) staff[0]; 暂时忽视对象的实际类型,使用对象的全部功能。超类强转为子类(类似golang中的断言成实际对象)。

将一个值存人变量时, 编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量, 编译器是允许的。但将一个超类的引用赋给一个子类变量, 必须进行类型转换, 这样才能够通过运行时的检査。

如果试图在继承链上进行向下的类型转换,并且“ 谎报” 有关对象包含的内容, 会发生什么情况呢?
Manager boss = (Manager) staff[1] ; // Error 运行这个程序时, Java 运行时系统将报告这个错误, 并产生一个ClassCastException异常。(golang中的断言失败)

在进行类型转换之前, 先查看一下是否能够成功地转换。这个过程简单地使用instanceof 操作符就可以实现。

// 父类 断言成 子类。父类是否是子类的一个实例?
if (staff[1] instanceof Manager)
{
	boss = (Manager) staff[1];
}

如果类型转换不可能成功,编译器就不会进行转换。

C++注释:类型转换像C++的dynamic_cast
Manager boss = (Manager) staff[1]; // java 等价于
Manager* boss = dynamic_cast<Manager*>(staff[1]); // c++ C++转换失败会得到一个NULL的boss。java失败会抛出一个异常。

抽象类:
public abstract String getDescription(); 抽象方法
包含抽象方法的必须是声明为抽象类。

public abstract class Person{
	public abstract String getDescription();
}

类似C++的纯虚函数。不可实例化。
和interface类似。

受保护访问:
下面归纳一下Java 用于控制可见性的4 个访问修饰符:
1 ) 仅对本类可见private
2 ) 对所有类可见public
3 ) 对本包和所有子类可见protected。与C++略有不同
实际上,protected的可见性在于两点:
1. 基类的protected成员是包内可见的,并且对子类可见;
2. 若子类与基类不在同一包中,那么在子类中,子类实例可以访问其从基类继承而来的protected方法,而不能访问基类实例的protected方法。
4 ) 对本包可见—默认(很遗憾),不需要修饰符。

5.2 Object: 所有类的超类

只有primitive types不是对象,其他都是对象。默认extends Object。
所有数组类型都扩展了Object类。

equals方法:
Object类:判断两个对象是否有相同的引用。

Objects.equals方法:可以涵盖对null的判断。

Objects.equals(name, other.name); // 可以hold住name, other.name为null的情况。都不为null时,调a.equals(b)

相等测试与继承:
问题:如果隐式和显式参数不属于同一个类,equals方法如何处理?
争议比较多,很多人用instanceof进行检测。没有解决otherObject是子类的情况。

Java要求equals方法具有下面特性:

  1. 自反性
  2. 对称性
  3. 传递性
  4. 一致性
  5. x.equals(null)为false

完美的方案:
下面给出编写一个完美的equals 方法的建议:
1 ) 显式参数命名为otherObject, 稍后需要将它转换成另一个叫做other 的变量。
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.getCIass()) return false; 如果所有的子类都拥有统一的语义,就使用instanceof 检测:
if (!(otherObject instanceof ClassName)) return false; 5 ) 将otherObject 转换为相应的类类型变量:
ClassName other = (ClassName) otherObject 6 ) 现在开始对所有需要比较的域进行比较了。使用= 比较基本类型域,使用equals 比较对象域。如果所有的域都匹配, 就返回true; 否则返回false。
return fieldl == other.field && Objects.equa1s(fie1d2, other.field2) 如果在子类中重新定义equals, 就要在其中包含调用super.equals(other)

hashCode方法:
Object默认:使用对象引用的存储地址
如果重写equals,那么hashCode一定也要重写!
如果x.equals(y)返回true,x.hashCode()一定要和y.hashCode()相同。
Objects,hash(name, salary, hireDay) ;可以组合多个hashCode。

java.util.Object
int hashCode( ); // 两个相等的对象要求返回相等的散列码。

java.util.Objects
static int hash(Object . . . objects ); //返回一个散列码,由提供的所有对象的散列码组合而得到
static int hashCode(Object a ); // 如果a 为null 返回0, 否则返回a.hashCode()

java.lang.(Integer|Long|Short|Byte|Double|Float|Character|Boolean)
static int hashCode( ( int |long|short|byte|double|float|char|boolean) value)

java.util.Arrays
static int hashCode( type[] a ); // 数组的散列码

toString:
返回表示对象值的字符串。
一般是
java.awt.Point[x=10,y=20],类名[值]
类名可以用getClass().getName()获得
只要用+连接字符串,就回去自动的调用toString方法。
x.toString() 等效于 ""+x,这样好处是x是基本类型也可以正常执行。

数组的toString是 [I@1a46e30 [I表示整形数组,修正方式是用 Arrays.toStringString s = Arrays.toString(luckyNumbers);
多维数组:Arrays.deepToString()方法

getClass:

java.lang.Object
Class getClass();
// 返回包含对象信息的类对象。Java 提供了类运行时的描述, 它的内容被封装在Class 类中。
java.lang.Class
String getName(); // 返回类名
Class getSuperclass(); // 返回超类信息

5.3 泛型数组列表 ArrayList<>

它使用起来有点像数组,但在添加或删除元素时, 具有自动调节数组容量的功能,而不需要为此编写任何代码。

ArrayList<Employee> staff = new ArrayList<Employee>(); // 解决运行时动态更改数组的问题
ArrayList<Employee> staff = new ArrayList<>(); // 菱形语法,编译器自动推导类型。
staff.add(new Employee("Harry Hacker", ...)); // 添加
staff.ensoureCapacity(100); // cap预分配100
ArrayList<Employee> staff = new ArrayList<>(100); // 初始容量为100
staff.size(); // 实际元素个数 等价于数组的a.length()
staff.trimToSize(); // 裁剪多余不用的空间,确认不会添加任何元素后再调trimToSize
staff.add(n, e); // 在第n之后插入e
staff.set(n, e); // 设置第n个元素
staff.get(n); // 获取第n个元素
staff.remove(n); // 删除第n个元素
for (Employee e : staff) // 遍历
X[] a = new X[list.size()]; list.toArray(a); // 将ArrayList转到数组中

菱形语法场景:编译器会检查新值是什么。如果赋值给一个变量, 或传递到某个方法, 或者从某个方法返回, 编译器会检査这个变量参数方法的泛型类型。

5.4 对象包装器与自动装箱

作用:基本类型变成一个对应类的对象。
不可变,且是final。可能是null。空指针警告!

list.add(3);
// 等价于:
list.add(Integer.valueOf(3)); // autoboxing

int n = list.get(i);
// 等价于:
int n = list.get(i).intValue(); // 拆箱

Integer n = 3; 
n++; // 拆箱,自增,再装箱

// 使用equals比较。

int x = Integer.parseInt(s);// 其他静态方法,比如字符串转整

// Integer不可变,传参修改无效,使用IntHolder传参修改

5.5 参数数量可变的方法

printf(String fmt , Object ...args);
等价于:
printf(String fmt , Object[] args);

System.out.printf("%d %s",new Integer(n) , "widgets");
等价于
System.out.printf("%d %s", new Object[]{ new Integer(n) , "widgets" });

5.6 枚举类

public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };

实际上, 这个声明定义的类型是一个类, 它刚好有4 个实例, 在此尽量不要构造新对象,直接用==比较。
所有enumclass都是继承Enum类。

enum Size
{
   SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL"); // 枚举值(构造函数参数)

   private Size(String abbreviation) { this.abbreviation = abbreviation; } // 私有构造函数
   public String getAbbreviation() { return abbreviation; } // Size类的方法。

   private String abbreviation; // 可以有域
}
java.lang.Enum <E>
static Enum valueOf (Class enumClass , String name) 
// toString的逆方法。返回指定名字、给定类的枚举常量。
String toString( )
// 返回枚举常量名。
int ordinal ( )
// 返回枚举常量在enum 声明中的位置,位置从0 开始计数。
int compareTo( E other )
// 如果枚举常量出现在Other 之前, 则返回一个负值;如果this=other,则返回0; 否则,
// 返回正值。枚举常量的出现次序在enum 声明中给出。

5.7 反射

reflection library提供了丰富且精心设计的工具集,能动态操纵Java代码的程序,大量用在JavaBeans中,Java组建的体系结构。
能在设计或运行中添加新类时,能够快速的应用开发工具动态地查询新添加类的能力。
反射机制可以用来:

  • 在运行时分析类的能力。
  • 在运行时查看对象, 例如, 编写一个toString 方法供所有类使用。
  • 实现通用的数组操作代码。
  • 利用Method 对象, 这个对象很像中的函数指针。
Class cl = xxx.getClass(); // 从对象获取类型信息。relect.TypeOf(xxx)
String className = "java.util.Randow";
cl.getName() // 返回类名
Class cl = Class.forName(className); // 用Class的静态方法forName获得类名对应的Class对象。

Class cl1 = Random.class; // 从类型获取类型信息

e.getClass().newInstance(); // 可以动态创建一个类的实例,使用默认构造器,没有默认构造器会抛出异常。
例如
Object m = Class.forName("java.util.Random").newInstance(); // 返回的是Object,需要转成Random

Constructor包中提供了用参数构造的能力,

java.lang.relect.Constructor
Object newInstance(Object[] args)

用反射分析类:

java.lang.class // getFields() getDeclaredFie1ds() getMethods() getDeclareMethods() getConstructors() getDeclaredConstructors()
java.lang.reflect.Field
java.lang.reflect.Method
java.lang.reflect.Constructor // 构造器。方法、域、参数、修饰符
java.lang.reflect.Modifier // 修饰符 isAbstract、Final、Interface、Native、Private、Protected、Public、Static、Strict、Synchronized、Volatile

运行时使用反射分析对象:
对象反射的修改。
即 golang中的 reflect.ValueOf(xxx)

// f is Field
Object v = f.get(Object) // 返回field的值信息。
f.setAccessible(true); // flag 为true 表明屏蔽Java 语言的访问检查,使得对象的私有属性也可以被査询和设置。
f.set(obj, value) // 可以将obj对象的f域设置成新值。

使用反射编写泛型数组代码:
java.lang.reflect.Array里的方法。

调用任意方法:
类似方法指针。Java设计者认为,方法指针是很危险的,常常带来隐患。可以用interface,或者反射。

Object invoke(Object obj, Object... args)
// 第一个参数是隐式参数(receiver),和golang的函数类型类似。对于静态方法,第一个参数是null。
例如:ml代表Employee的getName方法。
String n = (String)ml.invode(harry); // 无参数,对象是harry
// 如果返回类型是基本类型, invoke 方法会返回其包装器类型
// 可使用getDeclareMethods或Class类中的getMethod(函数名,Class实例)方法得到ml

建议仅在必要的时候才使用Method 对象,而最好使用接口以及Java SE 8 中的lambda 表达式(第6 章中介绍)。特别要重申: 建议Java 开发者不要使用Method 对象的回调功能。

5.8 继承的设计技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现“ is-a” 关系
  4. 除非所有继承的方法都有意义, 否则不要使用继承
  5. 在覆盖方法时, 不要改变预期的行为
  6. 使用多态, 而非类型信息
  7. 不要过多地使用反射

6 接口、lambda表达式与内部类

接口:C/C++没有的概念,但是golang有。
golang的接口参考:golang的接口 与golang的对比:

  1. 在 Java 中:实现接口需要显式的声明接口并实现所有方法;在 Go 中:实现接口的所有方法就隐式的实现了接口;从类型检查的过程来看,编译器仅在需要时才对类型进行检查,类型实现接口时只需要实现接口中的全部方法,不需要像 Java 等编程语言中一样显式声明。
  2. Java中的接口可以有静态方法、默认方法、常量。这比golang要方便些。

6.1 接口

例如:

// 泛型接口
public interface Comparable<T>
{
	int compareTo(T other) ; // parameter has type T
}

类在实现Comparable<Employee>时,必须提供以下方法:
int compareTo(Employee other) 注:接口中的所有方法自动地属于public!

接口不能含有实例域,不能被实例化(和golang相同)。

如果用Arrays.sort方法,但没有实现Comparable,编译不会报错,运行时会报错:
Exception in thread "main" java.lang.ClassCastException: interfaces.Employee cannot be cast to java.lang.Comparable

实用方法:static int compare(int x, int y) 对double也有。
注释:语言标准规定,x.compareTo(y)和y.compareTo(x)的符合必须相反。
ClassCastException()是类型不匹配时抛出的异常。

接口的特性:

type Comparable interface{
	// ...
	// golang不能包含常量
}
var x Comparable // 声明接口变量
x = &Employee{} // 接口引用实现接口的类对象
_, ok := anObject.(Comparable) {
	// 检测一个对象是否实现了一个接口
}
type ExtendComparable interface{
	Comparable // 直接组合扩展接口
	ExtendMethod()
}
public interface Comparable{
	// ...
}
Comparable x; // 声明接口变量
x = new Employee(); // 接口引用实现接口的类对象
if (anObject instanceof Comparable) {
	// 检测一个对象是否实现了一个接口
}
public interface ExtendComparable extends Comparable{ // extends扩展接口
	void ExtendMethod(); 
}

java特有的

  1. 实现了接口的,就有了这个接口声明的常量。然而,这样应用接口似乎有点偏离了接口概念的初衷, 最好不要这样使用它。
public interface Powered {
	// ...
	double SPEED_LIMIT = 95; // 接口里可以包含常量
	// 常量默认是public static final
}
  1. 实现多个接口(golang不存在这个问题)
    class Employee implements Cloneable, Comparable用逗号分隔。
  2. Java SE 8中,允许接口增加静态方法。(接口提供的静态方法)理论上讲,没有任何理由认为这是不合法的。只是这有违于将接口作为抽象规范的初衷。
    好处是不用再写一个类实现这个接口,直接在接口里写静态方法。
public interface Path
{
	public static Path get(String first, String... more) {
		return FileSystems.getDefault().getPath(first, more);
	}
	...
}
  1. 默认方法(类可以不用实现的方法)
    可以为接口方法提供一个默认实现。必须用default修饰符标记这样一个方法。(但是接口没有实例数据,实现的功能有限)
    使用场景:1.忽略其他接口,2.使用default调用非default方法,3.省略伴随类。4.接口演化(公共库的interface增加新方法,用default修饰后其他类不用适配)
public interface Collection
{
	int size() ; // An abstract method
	default boolean isEmpty()
	{
		return size() = 0;
	} . .
}

接口与抽象类:
C++注释:C++多重继承,所以可以用虚基类表达类似接口的概念,但是多重继承问题比较多。
Java如果用abstract class,则不能继承其他类。
接口:对功能的约定。类继承:对功能的继承。

解决默认方法冲突:
规则:

  1. 超类优先。超类提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略。
  2. 接口冲突。如果一个接口提供了一个默认方法, 另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突。

Java 设计者更强调一致性。

6.2 接口示例

  1. ActionListener的接口与回调
  2. Comparator接口作为sort的参数
  3. Clonable是一种标记接口,在Object中是protected(对本包和子类可见)

6.3 lambda表达式

带参数变量的表达式。

(String first, String second) -> first.length() - second.length()
// 参数 -> 实现

(String first, String second) ->
	{
		if (first.length() < second.length()) return -1;
		else if (first.length() > second.length()) return 1;
		else return 0;
	}
// 参数 -> {}

() -> { for (int i = 100; i >= 0;i--) System.out.println(i); }
// () -> {}

// 自动推导,如果可以推导出一个lambda 表达式的参数类型,则可以忽略其类型。例如:
Comparator<String> comp 
	= (first, second) // Same as (String first, String second)
		-> first.length() - second.length();
// 编译器发现被赋值给Comparator<String>,推导出first,second的类型。

函数式接口:
只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)。

不同于其他语言,在Java 中, 对lambda 表达式所能做的也只是能转换为函数式接口,不能赋值给Object变量,Object不是一个函数式接口

java.util.function中定义了很多非常通用的函数式接口。

import java.util.function;
BiFunction<String, String, Integer> comp = (first, second) -> first.length() - second.length();

// predicate接口
public interface Predicate<T>
{
	boolean test(T t);
	// Additional default and static methods
}
list.removeIf(e -> e == null);

方法引用(method reference)。
现成的方法可以完成你想要传递到其他代码的某个动作。

比如,Timer t = new Timer(1000, event -> System.out.println(event)): 可以把println直接传递进去就更方便了:Timer t = new Timer(1000, System.out::println) ;

System.out::println
等价于
x -> System.out.println(x)

// 三+一种情况
// 1. object::instanceMethod 对象的方法
System.out::println 等价于 x -> System.out.println(x)
this::equals等价于 x -> this.equals(x)

// 2. Class::staticMethod 类的静态方法
Math::pow 等价于 (x, y) -> Math.pow(x, y)

// 3. Class::instanceMethod 类的实例方法,x表示实例
String::compareToIgnoreCase 等价于 (x, y) -> x.compareToIgnoreCase(y)

// 4. Class::new 构造器引用
// 使用哪个构造器取决于接受者
// 数组构造器引用:
int[]::new 等价于 x->new int[x]
使用:构造泛型类型T 的数
Person[] people = stream.toArray(Person[]::new) :

lambda表达式捕获的值必须是最终变量(effectively final),指的是这个变量初始化之后就不会再为它赋新值。String对象就是合理的。
之所以有这个限制是有原因的。如果在lambda 表达式中改变变量, 并发执行多个动作时就会不安全。

处理lambda表达式:
要接受lambda表达式,需要选择(偶尔需要自己写)一个函数式接口。
例如:

public static void repeat(int n, Runnable action)
{
	for (int i = 0; i < n; i++) action.run() ;
}
repeat(10, 0 -> System.out.println("Hello, World!"));

注释: 如果设计你自己的接口,其中只有一个抽象方法, 可以用@FunctionalInterface 注解来标记这个接口。
// 函数式接口!!的注解

Comparator的一些静态方法:

Arrays.sort(people, Comparator.comparing(Person::getName)) ;

Arrays.sort(people,
	Comparator.comparing(Person::getlastName) // 串行比较器
		.thenConiparing(Person::getFirstName)) ;