枚举类型是一种特殊的数据类型,之所以特殊,是因为它既是一种类(Class)类型,却又比类类型多了一些特殊的约束,但正是因为这些约束的存在,也造就了枚举类型的简洁性、安全性、便捷性。
泛型,即“参数化类型”,参数,最熟悉的就是定义方法时有形参,调用此方法时传递实参。那么,如何理解参数化类型呢?顾名思义,参数化类型就是将类型由原来的具体类型参数化,类似于方法中的变量参数。此时类型也定义成参数形式(可以称之为类型形参),在调用/使用时传入具体的类型(类型实参)。
2.1 枚举类型
枚举类型是指其字段由一系列固定的常量组成的数据类型。在生活中常见这种类型的数据,例如,表示方向的值只能是东、西、南、北,表示一周中的天数只能是星期一 ~ 星期日。因为是常量,所以枚举类类型的字段要用大写字母表示。
2.1.1 枚举类型的定义
在Java语言中,使用关键字enum定义一个枚举类型。
例:使用枚举类型来指定一周中的天数
public enum Day{
星期日,星期一,星期二,星期三,星期四,星期五,星期六
}
在任何时候,如果需要代表一系列固定的常量,就应该使用枚举类型。在两种情况下,尽可能使用枚举类型:一种是自然的枚举类型,例如,表示太阳系中的行星或者员工的性别,这是一组 固定的值,所以在程序中应该使用枚举类型来表示;另一种是程序在编译的时候,已经知道某个数据所有可能的值,也要尽可能的使用枚举类型。
例如,在扑克牌游戏中,花色是固定的。只有四种颜色:方块,梅花,红心和黑桃,所以,在一个扑克牌游戏的程序中,如果要定义表示扑克牌花色的类型,可以使用枚举类型。
public enum Suit{
DIAMONDS, //方块
CLUBS, //梅花
HEARTS, //红心
SPADES, //黑桃
}
【例1】枚举示例类型
``
package enums;
public class EnumDemo {
Day day ; //声明Day枚举类型的变量day
public EnumDemo(Day day) { //构造器,传入一个Day类型的参数
this.day = day;
}
public void tellFelling() {
switch(day) {
case 星期一 : System.out.println("星期一都令人不喜欢");break;
case 星期五 : System.out.println("星期五所有人都喜欢");break;
case 星期六 :
case 星期日 : System.out.println("周末让人愉快");break;
default:System.out.println("一周的中间,不好也不坏");break;
}
}
public static void main(String[] args) {
EnumDemo firstDay = new EnumDemo(Day.星期一);
firstDay.tellFelling();
EnumDemo thirdDay = new EnumDemo(Day.星期二);
thirdDay.tellFelling();
EnumDemo fifthDay = new EnumDemo(Day.星期五);
fifthDay.tellFelling();
EnumDemo sixthDay = new EnumDemo(Day.星期六);
thirdDay.tellFelling();
EnumDemo seventhDay = new EnumDemo(Day.星期日);
thirdDay.tellFelling();
}
public enum Day{
星期日,星期一,星期二,星期三,星期四,星期五,星期六
}
}
``
2.1.2 枚举类型的迭代
在Java语言中,enum声明了一个类(称为“枚举类型”)。枚举类的类体中可能包括方法和其他字段。当编译器创建了一个枚举时,他会自动的添加一些专门的方法。例如,他会添加一个静态的values()方法,该方法会按照声明的顺序返回一个包含枚举类中所有值的数组。
values()方法通常与for-each结构一起使用,以迭代一个枚举类型的值。例如,要迭代一星期中的每一天,使用如下代码:
for(Day day :Day.values() ){
System.out.println("今天是"+day);
}
所有的枚举类都隐含地继承自java.lang,Enum。因为Java不支持多重继承,所有一个么句类型不能再从其他类继承。
【例2】枚举示例程序
public enum Planet{
水星(3.303e+23, 2.4397e6),
金星(4.869e+24, 6.0518e6),
地球(5.976e+24, 6.37814e6),
火星(6.421e+23, 3.3972e6),
木星(1.9e+27,7.1492e7),
土星(5.688e+26,6.0268e7),
天王星(8.686e+25, 2.5559e7),
海王星(1.024e+26,2.4746e7);
private final double mass;
private final double radius;
Planet (double mass, double radius){
this.mass = mass;
this.radius = radius;
}
public static final double G = 6.673008-11;
double surfaceGravity() {
return G* mass / (radius*radius);
}
double surfaceweight (double otherMass){
return otherMass * surfaceGravity();
}
public static void main(String[] args){
if (args.length<1){ 如果没有命令行参数,则返回
System.out.println("请在命令行中输入参数!");
return;
}
double earthweight = Double.parseDouble (args[0]);//通过命令行参数传递在地球上的休重
double mass = earthweight/地球.surfaceGravity(); //获取质量大小
for (Planet p:Planet.values())
System. out.printf("你在%s%f公斤。%n",p, p.surfaceweight (mass));
}
}
一个枚举类型的构造器必须为包级私有的或私有的(Private),它自动地创建在枚举体一开始定义的常量。不能人为的调用枚举类型的构造器。除属性和构造器外,Planet还定义了一些方法。
2.2 泛型
泛型是java语言中引入的新特性。通过在程序中使用泛型,可以提高程序代码的复用性,减少数据的类型转换,从而提高代码的运行效率,让代码更加健壮。
在任何正式的软件项目中,Bug都会存在,这是无法改变的事实。通过使用泛型,可以协助程序员进行代码检测,增加代码的稳定性。
2.2.1 为什么要使用泛型
程序中出现的Bug一般有两类,一类是编译时的Bug,另一类是运行时的Bug。满足一定的条件才会触发。在Java语言中引入泛型,通过在编译时进行更多的bug检测,从而增强代码的稳定性。
2.2.2 一个简单的Box类
public class Box{
private Object object;
public void add(Object object){
this.object = object;
}
public Object get(){
return object;
}
}
因为Box类的方法接收或返回Object,因此可以传入任何类型的参数(原始数据类型除外)。但是,如果这个“盒子“只用来存放和获取一个指定类型的对象,则将其需要包含的类型限定为某些指定的类型(如Interger),那么唯一的选择是在帮助文档中详细的说明这种要求(编译器对此一无所知)
public class BoxDemo1 {
public static void main(String[] args) {
//只能将Interger对象放入此箱子中
Box integerBox = new Box();
integerBox.add(new Integer(10));
Integer someInteger = (Integer)integerBox.get();
System.out.println(someInteger);
}
}
我们知道,将类型从Object转换到Interger是正确的,因为Object类型的引用实际上是Interger对象实例。但是编译器对此一无所知,它只信赖正确的转换,所以,如果传入一个错误类型的对象,如String,那么编译器并不知道这是错误的.
如上图所示,在从Box里获取放入的对象时,会想当然的任务应该是Interger类型的,从而将一个指向String类型的Objec引用变量向Interger类型转换。
这是一个Bug,但能编译通过。直到运行的时候,程序崩溃并抛出ClassCastException(程序转换异常)
2.3 泛型类型
泛型类型实际上是通过给类或接口增加类型参数(Type Parameters)来实现的。类型参数指明,泛型类型可以用在多种数据类型上,在具体使用时可以根据实际指定的类型来确定。
2.3.1 Box类型的泛型版本
声明泛型版本与声明类的一般版本的语法相似。只需要在类名后面加上一个类型参数即可。
将上一节中的Box改为泛型类,首先将声明类的代码"public class Box"改为“public class Box”,以创建一个“泛型类型声明”。
这里引入了一个“类型变量”称为T,可以在该类中的任何地方使用这些类型变量,在一定程度上将他们当做已知的类型。
实际上,“类型变量”T和一般意义上的变量非常相似,只需要把T想象为一个代表指定类别的变量即可,他的“变量值”是传递进来的任何类型:即可以是任何类类型,也可以是任何接口类型,甚至是另一个类型变量。但是它不能是任何的原始数据类型。
//Box 的泛型版本
public class Box<T>{
private T t ;
public void add(T t){
this.t = t;
}
public T get(){
return t;
}
}
在上面的代码中,将所有出现Object的地方都用T来代替,在程序中应用这个具有泛型特性的Box时,必须执行一个“泛型类型调用”,指名实际的具体类型,即在每个类型变量处分别用一个实际的具体类型替换掉T,例如使用Interger
Box <Interger> intergerBox;
一个泛型类型调用通常被看做“参数化类型”。要实例化这个类,照常使用new关键字,但是要将<Interger>
放在类名和圆括号之间。例如:
intergerBox = new Box<Interger>
Box<Interger> intergerBox = new Box<Interger>();
一旦intergerBox被实例化,就可以随便调用它的get()方法,而不再需要使用类型转换。
如下面的实例所示:
public class BoxDemo3 {
public staticvoid main(String[] args){
Bod<Integer> integerBox = new Box<Integer>();
integerBox.add(new Integer(10));
Integer someInteger = integerBox.get(); //不需要进行类型转换
System.out.println(someInteger);
}
}
2.3.2 参数类型命名惯例
按照惯例,类型参数命名为单个的大写字母。
这样命名的原因是:如果不这样命名,将很难区分一个类型变量和一个普通类或接口名称的不同。
- E : 元素。
- K :键
- N :数字
- T :类型
- V :值
- S U V:第二、三、四个类型。
2.4 泛型方法和泛型构造器
类型参数还可以再方法和构造器签名中声明,用来创建“泛型方法”和“”泛型构造器“。这与声明一个泛型类型相似,但是类型参数的作用域被限制在它被声明的方法或构造器中。
【例】泛型方法和泛型构造器示例
通过传递进来的不同类型,输出结果页相应的发生改变,
这种特性被称为"类型推断",允许像调用一个普通方法一样调用一个泛型方法,而不必指定一个类型。
2.5 限定的类型参数
有时候程序员想将允许被传递的类型参数的类型限制在一定的范围之内。例如,操作数字的方法可能只想接收Number(数字类)或其子类的实例。这就是“限定类型参数”的目的。
要声明一个限定的类型参数,可以通过列出类型参数的名字,后跟extends关键字。再后跟他的“上限”来实现。
【例】
对类型参数的限定也可以包括指定额外的必须被实现的接口,使用&字符来实现。如下所示:指定类型变量U可以接受的值为实现了MyInterface接口的Number类或其子类。
< U extends Number & MyInterface >
在限定类型参数时,可以限定上限的类型中最多只能有一个为类,其余必须为接口。接口可以有多个,中间用&字符分隔。
2.6 泛型子类型
可以将一个类型的对象赋给另一个类型的对象,只要两个类型是兼容的。
比如,可以将一个Interger类型的对象赋给一个Object类型的对象。因为Object是Interger的一个父类型
在面向对象的术语中,这被称为“是”的关系。但是,Interger还是一种Number。
对于泛型,也可以这样使用,执行一个泛型调用,传递Number作为它的类型实参,而任何后来对于add()方法的调用,如果参数与Number兼容(是Number类或其子类),则都是可以的。
现在考虑下面的方法:
public void boxTest(Box<Number> n){
}
它接收一个参数,参数的类型是’‘‘Box<Number - >’’’ ,但是Box<Interger - > 和 Box<Double - > 不是 ‘’‘Box<Number - >’‘’ 的子类型。
如果想象一个具体的对象——可以实际的描述事物——如笼子,理解为什么会这样会变得很容易。
在需要动物的地方,可以引用一头狮子,已同意狮子当然可以被放到一只关狮子的笼子里
Cage<Lion> lionCage = ...
lionCage.add(king)
而蝴蝶可以被放到一只关蝴蝶的笼子里。
Cage<Butterfly> lbutterflyCage = ...
lbutterflyCage.add(monarch)
那么,如果是一只“关动物的笼子”呢?或者准确的说,什么是一只“关所有动物的笼子”呢?
在这里,笼子被设计用来存放所有类型的动物,混合在一起。
不存在足够解释的格栅来关狮子,并且必须有足够小的空隙来关蝴蝶,这样一只笼子甚至不可能真正的建造出来。
2.7 使用通配
- 在泛型中,使用通配符 " ? "可以表示一个未知的类型。例如,要指定一只能关“某些”类型动物的笼子,可以使用下面的代码形式表示
Cage<? extends Animal> someCage = ... ;
将"? extends Animal" 读作“一个未知的类型,他是Animal的子类型,也可能是Animal本身”。
- 还可以通过使用super取代extends来指定一个下限。所以代码<? super Animal> 可以被读作‘一个未知的类型,它是Anmial的父类型,也可能是Anmial本身’
- 还可以使用一个“”无限定通配符<?>“来指定一个未知类型。无限定通配符在本质上与<? extends Object >相同
Cage< Lion > 和 Cage< Butterfly > 不是Cage< Animal > 的子类型。实际上他们是Cage< ? extends Animal >的子类型。
如果someCage 是一只关蝴蝶的笼子,那么它用来关蝴蝶很合适。但是如果用它来关狮子,则狮子会破窗而出。如果someCage是一只关狮子的笼子,那么用它来关狮子很合适,但是如果用来关蝴蝶,蝴蝶会飞走。s
所以,可以将各种动物放到他们单独的笼子里,并先为狮子调用这个方法,再为蝴蝶调用这个方法。
或者选择将所有动物组合放到关所有动物的笼子里。
2.8 类型擦除
当一个泛型被实例化时,编译器通过"类型擦除"的技术来编译这些类型。
“类型擦除”指的是编译器移除一个类或方法中所有与类型参数相关的信息。类型擦除能够使使用泛型的Java应用程序与Java类库在泛型出现之前创建的应用程序保持二进制上的兼容性。
Box< String > 被编译为类型Box。在这里,Box被称为是"原类型(Raw Type )".所谓源类型,指的是不带任何类型参数的泛型类接口或接口的名称。这意味着无法找出一个泛型类在运行时正在使用的是Object的什么类型。
因为存在“类型擦除”,所以新的代码可能继续使用遗留的代码。但不管什么原因,使用一个原类型被认为是一种不好的程序设计实践,应该尽量避免使用。
2.9 拓展训练
2.9.1 训练一:将枚举类型设置到集合中
2.9.2 训练二:输出枚举类类型
package list;
import list.DaysOfWeekEnum.DaysOfWeeK;
public abstract class TestWeek {
enum WeeK {
MONDAY{
public String getWeek() {
return "星期一";
}
},
TUESDAY{
public String getWeek() {
return "星期二";
}
} ,
WEDNESDAY{
public String getWeek() {
return "星期三";
}
},;
public abstract String getWeek();
}
public static void main(String[] args) {
for (WeeK w : WeeK.values()) {
System.out.println(w.ordinal() + "____" + w.name()+":" +w.getWeek());
}
}
}
2.10 技术解惑
2.10.1 EnumSet是什么
java.util.EnumSet是使用枚举类型的集合体现。当集合创建时,枚举集合中的所有元素必须来自单个指定的枚举类型,可以是显式的或者是隐式的。EnumSet是不同步的,不允许null值的存在。它也提供了一些有用的方法。如:copyOf(Collection c) \ of(Efirst) \ complementOf(EnumSet s)
2.10.2 使用泛型的好处是什么
泛型的本质是参数化类型。也就是数,所操作的数据类型被指定为一个参数。使用泛型的好处有:
(1)类型安全,提供编译期间的类型检测
(2) 前后兼容
(3)泛化代码,使代码可以被重复利用
(4)性能较高。可以为Java编译器和虚拟机带来更多的类型信息。