`
yunnick
  • 浏览: 392012 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Java枚举类-行为模式最佳实践

阅读更多

下面的主要内容是读了《Effective Java》第二版第30条之后的一些看法和总结。

 

     在面对一大篇的叙述性的知识点介绍时,往往觉得太过乏味,抓不住重点甚至有些力不从心。而采用对比的学习方式,可以明了孰优孰劣,关键特性是什么。第30条关于枚举的介绍,带给我一些感触。

曾和同事讨论如何使用常量,说就用过public static final,不知道枚举,唉╮(╯▽╰)╭是多不关注Java的特性更新啊!

     言归正传,首先就是介绍int枚举模式:

//The int enum pattern - severely deficient
public static final int APPLE_FUJI = 1;
public static final int APPLE_PIPPIN = 2;
public static final int APPLE_GRANNY_SMITH = 3;

public static final int ORANGE_NAVEL = 1;
public static final int ORANGE_TEMPLE = 2;
public static final int ORANGE_BLOOD = 3;

 

     这种方式存在诸多不便,你可以在需要APPLE_FUJI的地方使用ORANGE_NAVEL而不引起任何的编译和运行时异常,一旦常量值发生变化客户端必须重新编译,如果是混用的情况,运行时的行为即使重新编译也是无法确定的,而且往往在数据传递和使用时看到的只是个magic number,遍历int枚举常量也没有可靠的方法。

 

     还有String枚举模式,主要这种方式存在性能问题,因为依赖于字符串的比较操作。

     

     但有一种方式,就比如Integer.MAX_VALUE,Integer.MIN_VALUE,MATH.PI等,分别表示整型数的最大最小值和圆周率,是和具体的类相关联一个属性或者是一个有具体意义的常量值,这样表示是合适的。

   

    Java枚举类型背后的基本想法非常简单:它们就是通过共有的静态final域为每个枚举常量导出实例的类。它们是单例的泛型化,本质上时单元素的枚举。所以使用枚举实现单例是一种有效方式。相对于int枚举模式,Java枚举类型有以下优势:1、枚举类型拥有自己的命名空间,可以允许同名常量(不同命名空间);2、可以增加和重排枚举类型中常量,而无需重新编译客户端;(新增常量自然无法使用)3、toString可以获取常量的字面值;4、常量可任意添加方法和字段。

下面主要围绕文中提到的枚举常量的方法运用,进行下说明,并且发现文中提到的这些使用方式和设计模式的行为模式有些许相似,我称之为枚举的行为模式。

 

一、枚举常量的共同行为

以太阳系的8大行星为例,每颗行星都有质量和半径,通过这两个属性可以计算出表面重力,从而通过物体质量可以获取在某行星上重量,例子中枚举常量两个参数分别表示行星的质量和半径:

public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    EARTH  (5.975e+24, 6.378e6),
    MARS   (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);
    private final double mass;           // In kilograms
    private final double radius;         // In meters
    private final double surfaceGravity; // In m / s^2
 
    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;
 
    // Constructor
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }
 
    public double mass()           { return mass; }
    public double radius()         { return radius; }
    public double surfaceGravity() { return surfaceGravity; }
 
    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;  // F = ma
    }
}

 

下面是计算表面重量的主方法:

public class WeightTable {
	public static void main(String[] args) {
		double earthWeight = Double.parseDouble(args[0]);
		double mass = earthWeight / Planet.EARTH.surfaceGravity();
		for (Planet p : Planet.values())
		System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
	}
}

 

结果如下:

Weight on MERCURY is 66.133672

Weight on VENUS is 158.383926

Weight on EARTH is 175.000000

Weight on MARS is 66.430699

Weight on JUPITER is 442.693902

Weight on SATURN is 186.464970

Weight on URANUS is 158.349709

Weight on NEPTUNE is 198.846116

 

行为分析:

每个行星都要计算其表面重量,而行星计算表面重力的公式是不变的,所以每个常量的这个行为是统一的,抽象为一个方法即可,而巧妙之处在于在初始化常量时把表面重力值一同计算出来,计算时直接取值即可。

 

二、枚举常量的不同行为

 

以操作码为例,加减乘除的实现是不同的。首先是第一种实现方法(一种不好的方法):

// Enum type that switches on its own value - questionable
public enum Operation {
	PLUS, MINUS, TIMES, DIVIDE;
	// Do the arithmetic op represented by this constant
	double apply(double x, double y) {
		switch(this) {
		case PLUS: return x + y;
		case MINUS: return x - y;
		case TIMES: return x * y;
		case DIVIDE: return x / y;
		}
		throw new AssertionError("Unknown op: " + this);
	}
}

 

这段代码看似可行,但是却很脆弱,如果添加了新的常量,忘了给switch添加判断条件,编译没有问题,运行时却会报异常。而且以面向对象编程的角度来看,面对大量的switch case语句或者if else语句,那一般就是有改进的余地的。

文中提出了一种叫做“(特定常量方法实现)constant-specific method implementations”的一种方法,就是在枚举类型中声明一个抽象方法,在常量中进行实现,代码如下:

// Enum type with constant-specific method implementations
public enum Operation {
	PLUS { double apply(double x, double y){return x + y;} },
	MINUS { double apply(double x, double y){return x - y;} },
	TIMES { double apply(double x, double y){return x * y;} },
	DIVIDE { double apply(double x, double y){return x / y;} };
	abstract double apply(double x, double y);
}

 

这样添加新常量也不会忘记方法实现了,因为编译器会提醒你的。进一步改进就是与具体的常量数值结合起来,利用toString方便的打印算术表达式:

// Enum type with constant-specific class bodies and data
public enum Operation {
	PLUS("+") {
	double apply(double x, double y) { return x + y; }
	},
	MINUS("-") {
	double apply(double x, double y) { return x - y; }
	},
	TIMES("*") {
	double apply(double x, double y) { return x * y; }
	},
	DIVIDE("/") {
	double apply(double x, double y) { return x / y; }
	};
	private final String symbol;
	Operation(String symbol) { this.symbol = symbol; }
	@Override public String toString() { return symbol; }
	abstract double apply(double x, double y);
}
public static void main(String[] args) {
	double x = 2;
	double y = 4;
	for (Operation op : Operation.values())
	System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}

 

打印结果:

2.000000 + 4.000000 = 6.000000

2.000000 - 4.000000 = -2.000000

2.000000 * 4.000000 = 8.000000

2.000000 / 4.000000 = 0.500000

还建议写一个fromString()方法将字符串常量转为枚举常量。下面是可以参考的方式,利用Map方便实现字符串向枚举常量转换:

// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();
static { // Initialize map from constant name to enum constant
	for (Operation op : values())
	stringToEnum.put(op.toString(), op);
}
// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
	return stringToEnum.get(symbol);
}

 

三、基于策略的行为实现

3.1

上面的方式“constant-specific method implementations”有个不足之处,就是无法共享代码(似乎也很难共享代码),下面是一个可以共享代码的枚举例子:

以工资计算为例,五个工作日,八小时之外都算加班(T_T),算加班工资,下面是通过switch实现的一个方法:

// Enum that switches on its value to share code - questionable
enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
	private static final int HOURS_PER_SHIFT = 8;
	double pay(double hoursWorked, double payRate) {
		double basePay = hoursWorked * payRate;
		double overtimePay; // Calculate overtime pay
		switch(this) {
			case SATURDAY: case SUNDAY:
				overtimePay = hoursWorked * payRate / 2;
			default: // Weekdays
				overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
				break;
		}
		return basePay + overtimePay;
	}
}

 

不知道看到上面代码,有没有发现问题啊,就是周末加班的代码没有加break啊(坑)。从代码维护角度看,该代码很危险,如果添加一个新的枚举值(比如病假),忘了修改switch语句,计算工资肯定出错。

从面向对象的角度看,上面代码当然可以改进,文中就提出了一种“策略枚举”的方式实现工资计算,看着还真有点像策略模式呢,代码如下:

// The strategy enum pattern
enum PayrollDay {
	MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
	WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
	FRIDAY(PayType.WEEKDAY),
	SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
	private final PayType payType;
	PayrollDay(PayType payType) { this.payType = payType; }
	double pay(double hoursWorked, double payRate) {
		return payType.pay(hoursWorked, payRate);
	}
	// The strategy enum type
	private enum PayType {
		WEEKDAY {
			double overtimePay(double hours, double payRate) {
			return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2;
			}
		},
		WEEKEND {
			double overtimePay(double hours, double payRate) {
			return hours * payRate / 2;
			}
		};
		private static final int HOURS_PER_SHIFT = 8;
		abstract double overtimePay(double hrs, double payRate);
		double pay(double hoursWorked, double payRate) {
			double basePay = hoursWorked * payRate;
			return basePay + overtimePay(hoursWorked, payRate);
		}
	}
}

 

 

     PayType就是策略枚举,负责工资的计算,没有了Switch,增加工作日类型,选择计算策略很容易实现代码修改。

3.2

     还有一种方式实现,就和策略模式一模一样了,那就是基于对枚举类的扩展。枚举的扩展是通过接口实现的。比如以工资支付为例,已经有了工作日和周末的支付方式,我们想添加国庆加班工资支付方式,接口及实现如下:

//定义支付接口
public interface ShallPay {
	public double pay(double hours, double payRate);
}

 

//工作日周末支付实现
public enum PayType implements ShallPay {
	...
}

 

//国庆支付实现
public enum PayTypeHoliday  implements ShallPay {
	TRIPLE {
		public double pay(double hour, double payRate) {
			return 3*hour*payRate;
		}
	};
}

 

//具体的工作时间
public enum PayrollDay {
	public enum PayrollDay {
	MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
	WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
	FRIDAY(PayType.WEEKDAY),
	SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND),
	HOLIDAY(PayTypeHoliday.TRIPLE);
	
	private final ShallPay payType;
	PayrollDay(ShallPay payType) { this.payType = payType; }
	double pay(double hoursWorked, double payRate) {
		return payType.pay(hoursWorked, payRate);
	}	
}

 

     PayrollDay就是需要支付工资时的上下文环境,ShallPay就是策略接口,PayType,PayTypeHoliday的常量可看做具体策略的实现类。

这种方式,在原用枚举类的基础上,扩展自己的枚举类,就可以直接添加新的枚举类型实现相同接口,使用很方便。

 

注:

     用了上述方法后switch语句是否对枚举来说没有用了呢?文中提到对外部不受控的枚举使用switch是合适的,比如Operation枚举,不受你控制,希望有个方法返回运算符的反运算,可以用下列方式:

// Switch on an enum to simulate a missing method
public static Operation inverse(Operation op) {
	switch(op) {
		case PLUS: return Operation.MINUS;
		case MINUS: return Operation.PLUS;
		case TIMES: return Operation.DIVIDE;
		case DIVIDE: return Operation.TIMES;
		default: throw new AssertionError("Unknown op: " + op);
	}
}

 

2
1
分享到:
评论

相关推荐

    java-sington-factory-proxy 开发模式

    在Java编程中,设计模式是解决常见问题的模板化解决方案,它们是经验的结晶,能够提升代码的可读性、可维护性和复用性。本资源包主要涵盖了三种经典的设计模式:单例模式、工厂模式以及代理模式。下面将详细阐述这三...

    Java编程71-100例 Java编程71-100例

    在Java编程的学习过程中,实践是提升技能的关键。"Java编程71-100例"是一份涵盖了多种Java编程概念和技巧的...在学习过程中,务必关注每个例子的思路、设计模式以及最佳实践,这将有助于形成良好的编程习惯和思维方式。

    JAVA设计模式 JAVA设计模式

    Java设计模式是软件工程中的一种最佳实践,它是一种在特定情境下解决常见问题的经验总结。设计模式并不是具体的代码或库,而是对在编程中经常出现的某种问题的可复用解决方案的描述。这些模式通常涉及如何更好地实现...

    java常用技术整理-设计模式

    设计模式是软件工程中的一种最佳实践,它是在特定情境下为解决常见问题而形成的一套成熟的解决方案。在Java中,设计模式的应用对于提高代码的可读性、可维护性和复用性至关重要。以下是对Java中一些重要设计模式的...

    java设计模式-part1

    Java中,`java.util.Observable` 和 `java.util.Observer` 类实现了观察者模式。被观察的对象(Observable)会维护一个观察者列表,当状态变化时,调用`notifyObservers()`方法通知所有观察者(Observer)。 此外,...

    java-class-design-master (2).zip

    "java-class-design-master (2).zip" 压缩包很可能包含了一系列关于Java类设计的最佳实践、示例代码和可能的项目结构。让我们深入探讨一下Java类设计中的核心概念和重要知识点。 1. **面向对象原则**:Java是面向...

    设计模式-Java语言中的应用

    设计模式是软件工程中的一种最佳实践,它是在特定情境下为解决常见问题而形成的一套可复用的解决方案模板。在Java语言中,设计模式的应用广泛且深入,可以帮助开发者写出更加灵活、可维护和可扩展的代码。《设计模式...

    JAVA设计模式

    它们是经验的总结,代表了在特定上下文中被广泛接受的、经过时间考验的最佳实践。Java设计模式是面向对象编程中的一种重要概念,它们为解决常见的软件设计问题提供了模板。这份“JAVA设计模式”帮助文档将深入探讨...

    A-senior-Java-language-programming.rar_java programming

    9. **设计模式**:课程可能会涉及常见的设计模式,如工厂模式、单例模式、观察者模式等,这些都是解决软件设计中常见问题的最佳实践。 10. **JVM原理**:理解Java虚拟机的工作原理,包括类加载、内存管理和垃圾回收...

    java设计模式-javaDesignPatterns.zip

    Java设计模式是面向对象编程中的一种最佳实践,它是一套被广泛接受并应用于解决常见问题的解决方案模板。这些模式在软件工程中起到了重要的作用,它们是经验丰富的开发者们在面对特定设计挑战时积累的智慧结晶。...

    java_设计模式

    设计模式并非具体的代码或库,而是一种描述在特定情况下,如何设计和实现类或对象的最佳实践。Java设计模式的Chm版本为开发者提供了一个方便的离线阅读资料,包含了丰富的实例和解释,有助于提升代码质量和可维护性...

    java-basic-knowledge.zip_Knowledge

    以上只是"java-basic-knowledge.zip_Knowledge"可能涵盖的部分内容,实际文档可能还包含了更多深入的讲解和实例,如异常处理的最佳实践、集合框架的高级用法、设计模式的应用等。通过学习这份资料,初学者将能够建立...

    Java设计模式 影印

    Java设计模式是软件工程中的一种最佳实践,它们是解决常见编程问题的经验总结,为软件开发提供了可重用的解决方案。这些模式是经过多年实践和经验积累而形成的,它们可以帮助开发者更高效地编写可维护、可扩展的代码...

    java designpattern

    Java设计模式是软件开发中的重要概念,它是一种在特定情境下解决常见问题的模板或最佳实践。设计模式并不直接提供代码实现,而是描述在特定情况下,如何设计类和对象之间的关系,以达到可复用、可扩展和可维护的目标...

    java的设计模式 《设计模式-java语言中的应用.pdf》

    设计模式是软件工程中的一种最佳实践,它是在特定上下文中解决常见问题的模板或蓝图。在Java编程中,设计模式的应用极大地提高了代码的可读性、可维护性和可复用性。《设计模式——Java语言中的应用》这本书深入探讨...

    设计模式(包含java\c#\javascript等语言的设计模式)

    设计模式是软件工程中的一种标准解决方案,用于解决在开发过程中经常遇到的问题,它们代表了最佳实践,被广泛应用于各种编程语言中,如Java、C#和JavaScript。GOF 23设计模式,由Erich Gamma、Richard Helm、Ralph ...

    java 的研磨设计模式(模式)

    在编程领域,设计模式是一种被广泛认可的解决常见问题的最佳实践。Java的研磨设计模式是将这些模式应用到Java编程中的具体体现,它能够帮助开发者编写出更加灵活、可维护和可扩展的代码。本篇文章将深入探讨设计模式...

    Java 设计模式.zip

    Java设计模式是软件开发中的重要概念,它是一种在特定情境下解决常见问题的经验总结和最佳实践。这个开源项目"Java 设计模式.zip"显然是一个Java编程语言实现的设计模式集合,旨在帮助开发者理解和应用这些模式。...

    java与模式(附源码)

    在IT行业中,设计模式是软件开发中的重要概念,它代表了在特定场景下解决常见问题的最佳实践。Java作为广泛使用的编程语言,与设计模式的结合是提升代码质量和可维护性的关键。本资料“java与模式”包含了一系列关于...

    Java版设计模式教程

    本教程“Java版设计模式教程”专注于使用Java语言实现这些设计模式,帮助开发者深入理解面向对象编程中的最佳实践。 首先,我们来了解一下什么是面向对象编程(OOP)。面向对象编程是一种编程范式,它将现实世界的...

Global site tag (gtag.js) - Google Analytics