第1章 枚举类型
枚举类型的基本特性
- 调用枚举类型中的values()方法来遍历枚举常量;
- 编译器会为每一个自定义的枚举类型创建一个辅助类,这个类继承java.lang.Enum;
- Enum类实现了Comparable接口,所以自动包含compareTo()方法,另外还是先了Serializable接口。
在使用枚举类型时,可使用静态导入整个枚举类。在被导入的类里直接使用枚举常量。
在枚举类型里可自定义方法,并且可重载toString()方法,与重载普通类方法相同。
public enum EnumTest {
BOY,GIRL;
public static EnumTest[] getALlChildren(){
return values();
}
@Override
public String toString(){
return "EnumTest: boys , girls";
}
public static void main(String[] args) {
for (EnumTest enumTest : getALlChildren()){
System.out.println(enumTest);
}
}
}
values()方法:Enum类中并没有values()方法,该方法是由编译器在枚举类的定义中插入一个静态方法。
因为所有的枚举类型都继承java.lang.Enum,且Java只支持单继承,所以枚举类型之间不能通过继承 extends 来创建个新枚举对象。
但是可以通过 implements 同一个接口,来对枚举元素进行分类,如下例子,陆地动物和湖泊动物。
interface EnumInterface<T>{
EnumInterface[] getAllTEnums();
}
enum LandAnimal implements EnumInterface<LandAnimal>{
TIGER, FOX, ELEPHANT ;
@Override
public EnumInterface[] getAllTEnums() {
return values();
}
}
enum LAKEAnimal implements EnumInterface<LAKEAnimal>{
FISH_1, FISH_2;
@Override
public EnumInterface[] getAllTEnums() {
return values();
}
}
public class EnumTest {
public static void main(String[] args) {
EnumInterface[] enumInterfaces1 = getEnums(LandAnimal.FOX);
System.out.println(enumInterfaces1.length);
EnumInterface[] enumInterfaces2 = getEnums(LAKEAnimal.FISH_1);
System.out.println(enumInterfaces2.length);
}
static EnumInterface[] getEnums(EnumInterface enumInterface){
System.out.println(enumInterface);
return enumInterface.getAllTEnums();
}
}
/**
输出结果:
FOX
3
FISH_1
2
*/
如果单纯只对枚举元素进行分类,可将具体分类的类型枚举类,写在接口类内部,且都实现该接口,如下
interface Food{
enum Appetizer implements Food{
SALAD,SOUP
}
enum MainCourse implements Food{
LASANGE
}
}
且可以创建另一个枚举类,来管理其他枚举类型类,
enum Course{
APPETIZER(Food.Appetizer.class),
MainCourse(Food.MainCourse.class);
private Food[] values;
Course(Class<? extends Food> kind) {
values = kind.getEnumConstants();
}
}
EnumSet:
配合enum的使用,以代替传统的基于int的【位标识】用法
public static void main(String[] args) {
// 设置为空
EnumSet<Course> courseEnumSet = EnumSet.noneOf(Course.class);
// 添加一个
courseEnumSet.add(APPETIZER);
// 添加多个
courseEnumSet.addAll(EnumSet.of(APPETIZER, MainCourse));
}
EnumSet是基于64位的long构建的,每个枚举实例需要占用1位来表达是否存在的状态,这意味着在单词long的支撑范围内,1个EnumSet最多可支持包含64个元素的枚举类型。如果实际枚举类型个数超过64个,会引入一个新的long类型变量。
EnumMap:
特殊的map,要求所有的key都来自某个枚举类型。
EnumMap中的元素顺序由定义时的顺序决定。
EnumMap支持多路分发。
常量特定方法:
Java的枚举机制可以通过为每个枚举实例编写不同的方法,来赋予他们不同的行为。
定义抽象方法,要求每个元素要重写;
定义普通方法,指定的元素可以重写;
enum ConstantSpecificMethod{
DATE_TIME{
@Override
String getInfo() {
return DateFormat.getDateInstance().format(new Date());
}
},
JAVA_HOME {
@Override
String getInfo() {
return System.getenv("JAVA_HOME");
}
},
VERSION {
@Override
String getInfo() {
return System.getProperty("java.version");
}
};
// 定义方法, 然后让每个枚举元素去实现,
abstract String getInfo();
public static void main(String[] args) {
for (ConstantSpecificMethod method :values()){
System.out.println(method.getInfo());
}
}
}
/**
2022-9-12
D:\software\Java\jdk1.8.0_341
1.8.0_341
*/
职责链设计模式,先创建了一批用于解决目标问题的不同方法,然后将它们连成一条【链】。当一个请求到达时,会顺着这条链传下去,直到遇到链上某个可以处理该请求的方法。
枚举类型在职责链设计模式里的使用:
- 枚举类型里的每个元素就是职责里解决问题的不同方法;
- 枚举的定义顺序决定了各个问题被解决的顺序(如果循环枚举);
- 直到某个枚举元素下的方法执行返回成功,或者所有的枚举元素方法都执行失败。
枚举类型里各个元素下行为的切换,可以用来实现状态机,不同状态下不同的行为。
多路分发:
当要执行 a.plus(b),并且不知道a或b的具体类型时,如果保证他们间的相互作用是正确的呢?
Java本身只支持单路分发,即对上述问题,Java只能一个个检查类型是否满足要求。
多态只能在方法调用时发生,如果使用多路分发,就必须执行两次方法调用:第一次用来确定第一个未知类型,第二次用来确定第二个未知类型。
要使用多路分发,就必须对每个类型进行一次虚拟调用。如果是在操作两个不同的交互类型层次结构,则需要在每个层级结构都执行虚拟调用。
第2章 对象传递和返回
Java有指针,但是没有指针运算。Java的指针即是【引用】,可以认为它是安全的指针。
传递引用:
当将一个引用传给方法后,该引用指向的仍然是原来的对象。
引用别名指的是不止一个引用被绑定到了同一个对象上的情况。
public class ReferenceTest {
public static void main(String[] args) {
ReferenceTest test = new ReferenceTest();
System.out.println("outer : " + test);
f(test);
ReferenceTest test1 = test;
System.out.println("别名:" + test1);
}
static void f(ReferenceTest r){
System.out.println("inner:" + r);
}
}
/**
outer : com.br.test.lambda.ReferenceTest@1d81eb93
inner:com.br.test.lambda.ReferenceTest@1d81eb93
别名:com.br.test.lambda.ReferenceTest@1d81eb93
*/
上面代码的调用方法传对象和别名,都指向了同一个引用对象的地址。
创建本地副本:
Java中的所有参宿和传递都是通过传递引用实现的。当传递一个对象时,实际传递的是存活于方法外部的指向这个对象的一个引用。如果通过该引用执行了任何修改,同样也会修改外部的对象。此外:
- 引用别名会在传递参数时自动发生;
- 并没有本地对象,只有本地引用;
- 引用是有作用域的,对象则没有;
- Java中的对象生命周期从来就不是个问题。
值传递:有两种不同的说法。
- Java传递任何事物时,都是在传递该事物的值。当向方法传递基本类型时,得到的是该基本类型的一份单独的副本。而当你向方法传入一个引用时,得到的则是该引用的副本。
- Java在传递基本类型时,传递的是值,但在传递对象时,传递的则是引用。
克隆对象:
同一个对象的不同引用别名,是一种浅拷贝,因为这只复制了对象的【表层】部分。
实际对象的组成部分包括该表层引用,该引用指向的所有对象,以及所有这些对象所指向的所有对象,这通常称为对象网络。创建所有这些内容的完整副本,被称为深拷贝。
增加克隆能力:
1.待克隆的类要重写Object类的clone方法,并设置重写方法为public,方便外部调用。同时重写的方法内部一定要有super.clone();
2.待克隆的类要实现Cloneable接口,因为克隆时会检查是否实现此接口。没有会抛出CloneNotSupportedException异常。
【==】和【!=】操作只是简单地比较引用,如果引用内部地址相同,那么指向的就是同一个对象,因此就是“相等”的。
最终也是由根类的clone方法来为被克隆的对象创建正确大小的存储空间,并执行了从原始对象中的所有二进制位到新对象存储中的按位复制。
克隆组合对象:必须要克隆组合类里所有的对象引用,这些引用在组合类的clone方法里要调用自己的clone方法去生成。
public class ReferenceTest implements Cloneable{
private CombineClass combineClass;
public Object clone() throws CloneNotSupportedException {
ReferenceTest test = (ReferenceTest) super.clone();
test.combineClass = (CombineClass)test.combineClass.clone();
return test;
}
}
class CombineClass implements Cloneable{
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
一旦克隆了一个对象,就可以对副本进行修改了,不会影响到原副本。
序列化方式也可以实现深拷贝进行对象的clone,但是序列化方式要比clone方法慢一个数量级。
控制可克隆性:
一些方案:
- 不关心可克隆性,意味着该类无法被克隆,但是它的继承类可以在必要时增加克隆能力。
- 支持clone,实现Cloneable接口并重写clone方法。
- 视情况克隆。
- 不实现Cloneable接口,但将clone重写为protected的,并实现适用于所有字段的正确复制行为。
- 通过不实现Cloneable接口并重写clone(),使其抛出异常,来尽量避免克隆。
- 将类定义为final,以此阻止克隆。
// 无法克隆,未重写clone
class Ordinary{}
// 重写clone,但未实现Cloneable接口
class WrongClone extends Ordinary{
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
// 完美克隆
class IsCloneable extends Ordinary implements Cloneable{
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
// 通过抛出异常来关闭克隆
class NoMore extends IsCloneable{
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
// 通过调用父类的clone,抛出异常来关闭克隆
class TryMore extends NoMore{
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
// 重写clone,但未调用super.clone。更换为调用其他方法方式
class BackOn extends NoMore{
private BackOn duplicate(){
return new BackOn();
}
@Override
public Object clone(){
return duplicate();
}
}
// 无法继承该类,无法像在BackOn中一样重写clone()
final class ReallyNoMore extends NoMore{}
总结克隆一个类的步骤:
- 实现Cloneable接口;
- 重写clone()方法;
- 在clone()方法中调用super.clone()方法;
- 在clone()方法中捕获异常。
其他生成一个对象方式,创建这个待可控类的构造方法,参数就是这个待克隆的类,在构造方法里对把待克隆类的属性转移到新类中。
class Cat{
private String name;
private Integer age;
public Cat(){}
public Cat(Cat cat){
this.name = cat.getName();
this.age = cat.getAge();
}
public static void main(String[] args) {
Cat cat1 = new Cat();
Cat cat2 = new Cat(cat1);
}
}
不可变类:
指这个类在创建完毕后,对外暴露的都是可被读的方法,没有修改类内部属性的方法。
不可变类的缺点是,修改这种类型的对象时,就必须创建一个新对象来接收修改后的结果,并返回新对象。增加了资源开销。
解决方式是增加一个伴生类,修改放在这个伴生里中。前提是需要不可变对象、经常要做修改、或者创建新的不可变对象的开销很大。
String类本质就是不可变的,它的所有修改方法返回的结果都是重新创建一个String类接收。