Java复习3

第四部分:常用基础类

#1. 枚举类型介绍
Java的枚举类型用于在某些东西可以枚举出来的时候,我们可以把它定义为枚举类型,然后把其中包含的可选值一一枚举。打个比方,我们现在有一堆数字,大致可以分为四种类型,整型、浮点型、金额和百分比。整型的我就需要按整型显示,如果是浮点型则要求保留两位小数,如果是金额则要求按千分位展示,如果是百分比则要求显示对应的百分比。为此我们就定义了一个枚举类型NumberType,其一共四个枚举值,每个枚举值之间用逗号分隔,最后一个枚举值用分号结束(也可以不用,但是如果最后一个值后面还有其它内容时必须用分号结束)。
public enum NumberType {
   /**
    * 整型
    */

INTEGER,

/**

* 浮点型

*/

FLOAT,

/**

* 金额

*/

CURRENCY,

/**

* 百分比

*/

PERCENT;

}

对于这样的需求可能你觉得没必要使用枚举类型,我使用一个常量类,里面定义4个常量也是可以的,如下这样。

public class NumberTypes {

/**

* 整型

*/

public static final int INTEGER = 1;

/**

* 浮点型

*/

public static final int FLOAT = 2;

/**

* 金额

*/

public static final int CURRENCY = 3;

/**

* 百分比

*/

public static final int PERCENT = 4;

}

对的,定义一个常量类也可以达到对应的效果,但是在使用枚举类的时候将更加的方便。例如我们的类型是安全的,当你使用枚举类型时你传递的只能是对应的枚举类型里面包含的枚举元素,而你使用常量时你传递的类型只能是一个Integer,但是调用者完全有可能传入一个你没有定义的Integer。下面还会介绍一些枚举的功能,通过下面的介绍你就会觉得很多时候用枚举比用一个常量类功能更加的强大。

成员变量/构造方法
         枚举类型跟interface一样,也是Java类的一种,只是比较特殊的一种Java类。其也可以定义自己的成员变量、构造方法等。非枚举值必须定义在枚举值之后。如下,我们定义了一个成员变量code,和一个基于成员变量code的构造方法,我们的构造方法必须是private的,因为它是不能通过外部调用的。

public enum NumberType {

/**

* 整型

*/

INTEGER(1),

/**

* 浮点型

*/

FLOAT(2),

/**

* 金额

*/

CURRENCY(3),

/**

* 百分比

*/

PERCENT(4);

/**

* 成员变量,内部编码

*/

private int code;

/**

* 构造方法

* @param code

*/

private NumberType(int code) {

this.code = code;

}

}

对于枚举类型而言,其中定义的每一个枚举值就相当于对应枚举类型的一个具体对象,其在构造的时候必须利用对应枚举类型的一个构造方法来初始化。在上面的示例中我们只定义了一个构造方法,所以我们的每一个枚举值在构造的时候都只能通过传递code的方式来初始化。枚举类型的构造方法也是可以有多个的,如上面的示例中我们还可以给它加一个无参构造方法和其它构造方法。

成员方法
         枚举类型也可以定义自己的成员方法,其可以是public的,也可以是private的等。如下,我们定义了一个公用的getCode()方法,其在方法体里面返回了成员变量code。

public enum NumberType {

/**

* 整型

*/

INTEGER(1),

/**

* 浮点型

*/

FLOAT(2),

/**

* 金额

*/

CURRENCY(3),

/**

* 百分比

*/

PERCENT;

/**

* 成员变量,内部编码

*/

private intcode;

/**

* 无参构造,没意义

*/

private NumberType() {}

/**

* 构造方法

* @param code

*/

private NumberType(int code) {

this.code = code;

}

/**

* 获取对应的编码

* @return

*/

public int getCode() {

return this.code;

}

}

除了这种非抽象方法之外,枚举也可以定义抽象方法,抽象方法则要求每一个枚举值都必须实现。如下,当我们有一个需求就是根据不同类型的枚举值把对应的数据展示为不同的形式时我们就可以定义一个对应的show抽象方法,然后由各个枚举值根据自身的需求实现。

public enum NumberType {

/**

* 整型

*/

INTEGER(1) {

@Override

public String show(Object value) {

//只返回整型部分,去掉小数之类的

return null;

}

},

/**

* 浮点型

*/

FLOAT(2) {

@Override

public String show(Object value) {

//必须保留两位小数

return null;

}

},

/**

* 金额

*/

CURRENCY(3) {

@Override

public String show(Object value) {

//必须展示千分位

return null;

}

},

/**

* 百分比

*/

PERCENT {

@Override

public String show(Object value) {

//必须展示成百分比的形式,0.15变成15%

return null;

}

};

/**

* 成员变量,内部编码

*/

private int code;

/**

* 无参构造,没意义

*/

private NumberType() {}

/**

* 构造方法

* @param code

*/

private NumberType(int code) {

this.code = code;

}

/**

* 获取对应的编码

* @return

*/

public int getCode() {

return this.code;

}

/**

* 按照特定的形式展示数据

* @param value

* @return

*/

public abstract String show(Object value);

}

自带的成员方法
         所有的枚举类型定义实际上都是隐式的继承了java.lang.Enum类作为自己的基类型的。Enum类型中主要定义了三个公用的成员方法。

name():返回对应的元素名称。

ordinal():返回对应的元素在枚举类型中定义的顺序号,第一个元素的顺序号是0。

compareTo():用来比较两个枚举类型对象,其返回结果是调用两个对象的ordinal()返回值进行相减。

静态方法
         枚举类型也可以包含静态方法,如下示例中我们就定义了一个枚举类型NumberType的静态方法,get方法,通过NumberType的code来获取对应的NumberType。

public enum NumberType {

/**

* 整型

*/

INTEGER(1),

/**

* 浮点型

*/

FLOAT(2),

/**

* 金额

*/

CURRENCY(3),

/**

* 百分比

*/

PERCENT(4);

/**

* 成员变量,内部编码

*/

private int code;

/**

* 无参构造,没意义

*/

private NumberType() {}

/**

* 构造方法

* @param code

*/

private NumberType(int code) {

this.code = code;

}

/**

* 获取对应的编码

* @return

*/

public int getCode() {

return this.code;

}

/**

* 通过code来获取对应的NumberType

* @param code

* @return

*/

public static NumberType get(int code) {

NumberType[] values = NumberType.values();

for (NumberType value : values) {

if (value.getCode() == code) {

return value;

}

}

return null;

}

}

自带的静态方法
         上一个示例中我们自己定义了枚举类型NumberType的静态方法,get方法。而实际上,在Java中所有的枚举类型都会自带三个静态方法。

values():获取当前枚举类型里面所有的元素,如上面示例中我们get静态方法的应用。

valueOf(String name):根据元素的名称来获取当前枚举类型里面的元素。

valueOf(Class enumType, String name):根据元素名称来获取指定枚举类型里面的某个元素。

switch枚举类型
         我们在使用枚举类型的时候如果要具体用到某个元素,我们都是通过“enum.ele”的形式来获取具体的某个元素应用的,如我们需要使用枚举类型NumberType的CURRENCY元素时,我们会使用NumberType.CURRENCY的形式来使用。但是在使用switch语句对枚举类型进行逻辑处理的时候,我们就不能再加上对应的枚举类型前缀了,而是需要直接使用对应的元素。如我们需要在switch语句中使用上述的NumberType时,我们应该以如下的方式来使用。

public void doSwitch(NumberType numberType) {

switch (numberType) {

case INTEGER:

System.out.println(“整型”);

break;

case FLOAT:

System.out.println(“浮点型”);

break;

case CURRENCY:

System.out.println(“金额”);

break;

case PERCENT:

System.out.println(“百分比”);

break;

}

}

EnumMap
         有的时候我们可能需要把我们的枚举类型作为一个Map的Key来使用,这种情况的话官方建议我们直接使用EnumMap。EnumMap是Java专门为以枚举类型作为Key来构造的一个Map,其底层的元素是以数组的方式来存储的。这也比较好理解,因为枚举类型的元素一般不会很多,就算很多的话它的key也是固定的,所以EnumMap在初始化的时候就有一个数组与之对应了,数组的长度就是对应枚举类型的元素个数。EnumMap中对应的Key的值存储的就是存储在数组中索引为枚举类型元素的ordinal()的位置。

EnumSet
         有的时候我们可能需要把我们的枚举类型对象存入到Set中,这种情况的话官方建议我们直接使用EnumSet。EnumSet在使用的时候不能直接new,它提供了一系列的静态of方法,用以基于某类型的枚举的某些元素来创建一个EnumSet,其返回结果都是一个EnumSet对象。

EnumSet enumSet = EnumSet.of(NumberType.CURRENCY, NumberType.PERCENT);

#4. String字符串
java中字符串常用的方法:
public class HelloWorld {
public static void main(String[] args) {
String s1=new String (“爸爸”);
String s2=“爸爸2”;
String s3=“abcdefghigk”;
//返回指定索引处的 char 值
char c=s3.charAt(5);
System.out.println©;
// 返回指定索引处的字符(Unicode 代码点)。
int i=s3.codePointAt(3);
System.out.println(i);
// 返回指定索引之前的字符(Unicode 代码点)。
System.out.println(s3.codePointBefore(2));
//按字典顺序比较两个字符串。
System.out.println(s2.compareTo(s1));
//按字典顺序比较两个字符串,不考虑大小写。
System.out.println(s2.compareToIgnoreCase(s1));
//将指定字符串连接到此字符串的结尾
System.out.println(s2.concat(s3));
// 当且仅当此字符串包含指定的 char 值序列时,返回 true。
System.out.println(s3.contains(“def”));
//测试此字符串是否以指定的后缀结束。
System.out.println(s3.endsWith(“dasd”));
//将此字符串与指定的对象比较。
System.out.println(s1.equals(s2));
//使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
byte[] b1=s3.getBytes();
for (byte b:b1
) {
System.out.println(b);
}
//将字符从此字符串复制到目标字符数组。
char[] s4=new char[10];
s3.getChars(1,5,s4,5);
// 返回此字符串的哈希码。
System.out.println(s3.hashCode());
// 返回指定字符在此字符串中第一次出现处的索引。
System.out.println(s3.indexOf(5));
// 返回此字符串的长度。
System.out.println(s3.length());
//使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
System.out.println(s3.replace(“abc”,“def”));
//将此字符串转换为一个新的字符数组
char[] ch=s3.toCharArray();
for (char c5:ch
) {
System.out.print(c5);
}
//使用默认语言环境的规则将此 String 中的所有字符都转换为大写。
System.out.println(s3.toUpperCase());

}



}

#5. 字符串缓冲区StringBuffer和StringBuilder
# StringBuffer特点:
1、是字符缓冲区,一个容器(不论缓冲区内容怎么变化,StringBuffer这个容器对象都不会改变)
2、是可变长度的
3、可以直接操作多个数据类型
4、最终会通过toString()方法变成字符串

@存储:StringBuffer append():将指定的数据作为参数添加到已有的数据结尾处。
  StringBuffer insert(index,数据):可以将数据插入到指定的index位置处。
 
@删除:StringBuffer delete(Start,end):删除缓冲区的数据,包含头start,不包含尾end
  StringBuffer deleteCharAt(int index):删除指定位置的字符
 
@获取: char charAt(int index) 
   int indexOf(String str)
   int indexOf(String str, int fromIndex) 
  int lastIndexOf(String str) 
   int lastIndexOf(String str, int fromIndex)  
  int length()  
  String substring(int start)  
  String substring(int start, int end) 
  String toString() 
 
@修改:StringBuffer replace(int start, int end, String str) :将指定位置的字符串用其他的字符串替换掉
  void setCharAt(int index, char ch):替换一个字符 
 
@反转: StringBuffer reverse():将容器中的字符串进行反转
 
@将缓冲区的指定数据存放到指定数组的指定位置中: void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
     
	StringBuilder.
 
相同点:方法功能全部一样
不同点:StringBuffer 是线程同步的,具有锁(适用于多线程)
StringBuilder 是线程不同步的,没有锁(适用于单线程)

注意:以后开发建议使用StringBuilder,因为它提高了效率

#6. 字符串与缓冲区的区别
三者在执行速度方面的比较:
StringBuilder > StringBuffer > String
(StringBuffer,StringBuilder)> String的原因
    String:字符串常量
    StringBuffer:字符串变量
    StringBuilder:字符串变量
  从上面的名字可以看到,String是“字符串常量”,也就是不可改变的对象。对于这句话的理解你可能会产生这样一个疑问 ,比如这段代码:
String s = “abcd”;
s = s+1;
System.out.print(s);// result : abcd1
我们明明就是改变了String型的变量s的,为什么说是没有改变呢? 其实这是一种欺骗,JVM是这样解析这段代码的:
  首先创建对象s,赋予一个abcd,然后再创建一个新的对象s用来执行第二行代码,也就是说我们之前对象s并没有变化,所以我们说String类型是不可改变的对象了,由于这种机制,每当用String操作字符串时,实际上是在不断的创建新的对象,而原来的对象就会变为垃圾被GC回收掉,可想而知这样执行效率会有多底。
  而StringBuffer与StringBuilder就不一样了,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,这样就不会像String一样创建一些而外的对象进行操作了,当然速度就快了。

3.一个特殊的例子:

String str ="This is only a" + "simple" + "test";
StringBuffer builder = new StringBuilder("This is only a").append(" simple").append("test");
你会很惊讶的发现,生成str对象的速度简直太快了,而这个时候StringBuffer居然速度上根本一点都不占优势。其实这是JVM的一个把戏,实际上:

String str = "This is only a" + "simple"+"test";
其实就是:

String str = "This is only a simple test";
所以不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的String对象的话,速度就没那么快了,譬如:

String str2 = " This is only a";
String str3 = " simple";
String str4 = " test";
String str1 = str2 + str3 + str4;
这时候JVM会规规矩矩的按照原来的方式去做。

4.StringBuilder与 StringBuffer
  StringBuilder:线程不安全的
  StringBuffer:线程安全的
  当我们在字符串缓冲区被多个线程使用是,JVM不能保证StringBuilder的操作是安全的,虽然他的速度最快。当然大多数情况下就是我们是在单线程下进行的操作,所以大多数情况下是建议用StringBuilder而不用StringBuffer的,就是速度的原因。

对于三者使用的总结:
  1.如果要操作少量的数据用 = String
  2.单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
  3.多线程操作字符串缓冲区 下操作大量数据 = StringBuffer</pre>

#7. 基本类型的包装类型

1.Java的基本类型及其对应的包装器类
Java有8种基本类型:大致分为3类:字符,布尔,数值类型(在java中数值是不存在无符号的,这一点不像C/C++,他们的取值范围是固定的,不会随着机器硬件的环境或者操作系统的改变而改变)

byte:8位,最大存储数据量是255,存放的数据范围是-128~127之间。
short:16位,最大数据存储量是65536,数据范围是-32768~32767之间。
int:32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。
long:64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。
float:32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。
double:64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。
boolean:只有true和false两个取值。
1.Java决定了每种简单类型的大小。这些大小并不随着机器结构的变化而变化。这种大小的不可更改正是Java程序具有很强移植能力的原因之一。下表列出了Java中定义的简单类型、占用二进制位数及对应的封装器类。

基本类型 二进制位数 包装器类
boolean 1 Boolean
byte 8 Byte
char 16 Character
short 16 Short
int 32 Integer
long 64 Long
float 32 Float
double 64 Double
2.对于取值范围,在对应的包装器类中有常量记载,直接调用就可以了,无需记忆

基本类型byte 二进制位数:Byte.SIZE最小值:Byte.MIN_VALUE最大值:Byte.MAX_VALUE
基本类型short二进制位数:Short.SIZE最小值:Short.MIN_VALUE最大值:Short.MAX_VALUE
基本类型char二进制位数:Character.SIZE最小值:Character.MIN_VALUE最大值:Character.MAX_VALUE
基本类型double 二进制位数:Double.SIZE最小值:Double.MIN_VALUE最大值:Double.MAX_VALUE
Java基本类型存储在栈中,因此它们的存取速度要快于存储在堆中的对应包装类的实例对象。从Java5.0(1.5)开始,JAVA虚拟机(Java Virtual Machine)可以完成基本类型和它们对应包装类之间的自动转换。因此我们在赋值、参数传递以及数学运算的时候像使用基本类型一样使用它们的包装类,但这并不意味着你可以通过基本类型调用它们的包装类才具有的方法。另外,所有基本类型(包括void)的包装类都使用了final修饰,因此我们无法继承它们扩展新的类,也无法重写它们的任何方法。

3.基本类型出现的原因
在Java编程思想的第一章就讲到:万物皆对象,new一个对象存储在堆中,我们通过堆栈的引用来使用这些对象,但是对于经常用到的一系列类型如int,如果我们用new将其存储在堆里就不是很有效——特别是简单的小的变量。所以就出现了基本类型,同C++一样,Java采用了相似的做法,对于这些类型不是用new关键字来创建,而是直接将变量的值存储在堆栈中,因此更加高效。

4.包装类型出现的原因
Java是一个面向对象的语言,基本类型并不具有对象的性质,为了与其他对象“接轨”就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

5.关于基本类型和包装类型的总结

基本类型的优势:数据存储相对简单,运算效率比较高
包装类的优势:有的容易,比如集合的元素必须是对象类型,满足了java一切皆是对象的思想
声明方式不同,基本类型不适用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
存储方式及位置不同,基本类型是直接将变量值存储在堆栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
初始值不同,基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null
使用方式不同,基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到
2.Java类型转换
1.自动转换
2.强制转换
3.包装器过渡类型转换
4.字符串与其他类型之间的转换
5.Date和其他数据类型之间的转换
import java.text.SimpleDateFormat;
import java.util.Date;

public class Demo01 {

/**
 * @param args
 */
public static void main(String[] args) {
    // TODO Auto-generated method stub

    //1.自动转换
    byte b1 = 12;
    int a = b1*2;          //这里发生了自动转换

    //2.强制转换测试
    byte byte1 = 12;
    byte byte2 = 1;
    byte i =(byte)(byte1 + byte1);     //显示不能从byte转换成int  这里需要强制转换

    //3.包装器过渡转换
    String s = "123";
    int value = Integer.parseInt(s);    //方法返回int值赋给value

    //4.字符串和其他类型的转换
    int num = 2017;
    String time= ""+num;              //右边最终结果是String

    String str1 = "123";
    int value1 = Integer.valueOf(str1);  //结果是int 123

    //5.Date与其他数据类型的相互转换
    Date date = new Date();   
    SimpleDateFormat format = new SimpleDateFormat("yyyyMMDD");
    String timeString = format.format(date);

}

}

总结:只有boolean不参与数据类型的转换

(1).自动类型的转换:
a.常数在表数范围内是能够自动类型转换的
b.数据范围小的能够自动数据类型大的转换(注意特例)int到float,long到float,long到double 是不会自动转换的,不然将会丢失精度
c.引用类型能够自动转换为父类的
d.基本类型和它们包装类型是能够互相转换的

(2).强制类型转换:用圆括号括起来目标类型,置于变量前
3.Java引用类型
Java有 5种引用类型(对象类型):类 接口 数组 枚举 标注
引用类型:底层结构和基本类型差别较大
JVM的内存空间:
(1). Heap 堆空间:分配对象 new Student()
(2). Stack 栈空间:临时变量 Student stu
(3).Code 代码区 :类的定义,静态资源 Student.class

Student stu = new Student(); //new 在内存的堆空间创建对象
stu.study(); //把对象的地址赋给stu引用变量
上例实现步骤:
a.JVM加载Student.class 到Code区
b.new Student()在堆空间分配空间并创建一个Student实例
c.将此实例的地址赋值给引用stu, 栈空间

4.Java包装类的一些常见知识点归纳
1.Java包装器比较大小(很有意思的问题)
public static void main(String[] args) {
Integer a = new Integer(100);
Integer b = new Integer(100);
/* compareTo返回值:若a>b则返回1;若a==b则返回0;若a<b则返回-1 */
int result = a.compareTo(b);

 System.out.println(a > b);
 System.out.println(a == b);
 System.out.println(a > b);
 System.out.println(result);

}

运行结果:
false
false
false
0

为什么(a==b)返回值会是false呢?

通过对比字符串比较来理解,基本类型100通过包装类Integer包装后生产一个Integer对象的引用a,而“==”使用来判断两个操作数是否有相等关系。如果是基本类型就直接判断其值是否相等。若是对象就判断是否是同一个对象的引用,显然我们new了两个不同的对象。但注意:对于”<”,”>” 只是用来判断两个基本类型的数值的大小关系。在进行”(a < b)运算时,实际上是根据其 intValue方法的返回对应的数值来进行比较的。因此返回肯定是false.

知道问题原因,解决起来就容易了。两种方法:
第一种: a.intValue()==b.intValue();
第二种: a.compareTo(b);//返回-1代码(a

public int compareTo(Integer object) {
int thisValue = value;
int thatValue = object.value;
return thisValue < thatValue ? -1 : (thisValue == thatValue ? 0 : 1);
}

2.Java包装类常量池
Java的8种基本类型(Byte, Short, Integer, Long, Character, Boolean, Float, Double), 除Float和Double以外, 其它六种都实现了常量池, 但是它们只在大于等于-128并且小于等于127时才使用常量池。
public static void main(String[] args) {
//常量池的测试
System.out.println(“Integer的测试”);
Integer a = 127;
Integer b = 127;
System.out.println(a == b);

    a = 128;  
    b = 128;   
    System.out.println(a == b);   

    a = -128;  
    b = -128;   
    System.out.println(a == b);  

    a = -129;  
    b = -129;   
    System.out.println(a == b);  

    // 测试Boolean  
    System.out.println("测试Boolean");  
    Boolean c = true;  
    Boolean d = true;  
    System.out.println(c == d);  
    d = new Boolean(true);  
    System.out.println(c == d);  

}
程序运行结果
Integer的测试
true
false
true
false
测试Boolean
true
false

当我们给Integer赋值时,实际上调用了Integer.valueOf(int)方法(编译期进行装箱),查看源码,其实现如下:

public static Integer valueOf(int i) {  
if(i >= -128 && i <= IntegerCache.high)  
return IntegerCache.cache[i + 128];  
else  
return new Integer(i);  
}  

而IntegerCache实现如下

private static class IntegerCache {  
static final int high;  
static final Integer cache[];  
static {  
    final int low = -128;  
    // high value may be configured by property  
    int h = 127;  
    if (integerCacheHighPropValue != null) {  
        // Use Long.decode here to avoid invoking methods that  
        // require Integer's autoboxing cache to be initialized  
        int i = Long.decode(integerCacheHighPropValue).intValue();  
        i = Math.max(i, 127);  
        // Maximum array size is Integer.MAX_VALUE  
        h = Math.min(i, Integer.MAX_VALUE - -low);  
    }  
    high = h;  
    cache = new Integer[(high - low) + 1];  
    int j = low;  
    for(int k = 0; k < cache.length; k++)  
        cache[k] = new Integer(j++);  
}  

private IntegerCache() {}  

}

注意cache数组是静态的。
下面的语句不会缓存常量:
Integer integer = new Integer(127);
因为使用了new关键字,一定是创建了一个新的对象,无法进行缓存优化。

  • 3.Java基本类型和包装类型使用运算符 == 进行比较的底层细节? 是引用还是至比较值?
    public class IntegerCompareTest {

    /**

    • @param args
      */
      public static void main(String[] args) {
      // TODO Auto-generated method stub
      Object obj = new Integer(1024);
      if(Integer.valueOf(obj.toString())==1024){
      System.out.println(“Integer.valueOf(obj.toString())==1024 is true”);
      }
      }
      }

通过Javap.exe进行反编译得到的JVM指令如下所示

Classfile /C:/Users/JIN/Documents/GitHub/JavaBasePractice/bin/com/jinwen/basetype/IntegerCompareTest.class
  Last modified 2017-7-19; size 906 bytes
  MD5 checksum d0968e0c73408dd3cb83daddfe8e1a1b
  Compiled from "IntegerCompareTest.java"
public class com.jinwen.basetype.IntegerCompareTest
  SourceFile: "IntegerCompareTest.java"
  minor version: 0
  major version: 50
  flags: ACC_PUBLIC, ACC_SUPER

Constant pool:
   #1 = Class  #2 //  com/jinwen/basetype/IntegerCompareTest
   #2 = Utf8   com/jinwen/basetype/IntegerCompareTest
   #3 = Class  #4 //  java/lang/Object
   #4 = Utf8   java/lang/Object
   #5 = Utf8   <init>
   #6 = Utf8   ()V
   #7 = Utf8   Code
   #8 = Methodref  #3.#9  //  java/lang/Object."<init>":()V
   #9 = NameAndType#5:#6  //  "<init>":()V
  #10 = Utf8   LineNumberTable
  #11 = Utf8   LocalVariableTable
  #12 = Utf8   this
  #13 = Utf8   Lcom/jinwen/basetype/IntegerCompareTest;
  #14 = Utf8   main
  #15 = Utf8   ([Ljava/lang/String;)V
  #16 = Class  #17//  java/lang/Integer
  #17 = Utf8   java/lang/Integer
  #18 = Methodref  #16.#19//  java/lang/Integer."<init>":(I)V
  #19 = NameAndType#5:#20 //  "<init>":(I)V
  #20 = Utf8   (I)V
  #21 = Methodref  #3.#22 //  java/lang/Object.toString:()Ljava/lang/String;
  #22 = NameAndType#23:#24//  toString:()Ljava/lang/String;
  #23 = Utf8   toString
  #24 = Utf8   ()Ljava/lang/String;
  #25 = Methodref  #16.#26//  java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
  #26 = NameAndType#27:#28//  valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
  #27 = Utf8   valueOf
  #28 = Utf8   (Ljava/lang/String;)Ljava/lang/Integer;
  #29 = Methodref  #16.#30//  java/lang/Integer.intValue:()I
  #30 = NameAndType#31:#32//  intValue:()I
  #31 = Utf8   intValue
  #32 = Utf8   ()I
  #33 = Fieldref   #34.#36//  java/lang/System.out:Ljava/io/PrintStream;
  #34 = Class  #35//  java/lang/System
  #35 = Utf8   java/lang/System
  #36 = NameAndType#37:#38//  out:Ljava/io/PrintStream;
  #37 = Utf8   out
  #38 = Utf8   Ljava/io/PrintStream;
  #39 = String #40//  Integer.valueOf(obj.toString())==1024 is true
  #40 = Utf8   Integer.valueOf(obj.toString())==1024 is true
  #41 = Methodref  #42.#44//  java/io/PrintStream.println:(Ljava/lang/String;)V
  #42 = Class  #43//  java/io/PrintStream
  #43 = Utf8   java/io/PrintStream
  #44 = NameAndType#45:#46//  println:(Ljava/lang/String;)V
  #45 = Utf8   println
  #46 = Utf8   (Ljava/lang/String;)V
  #47 = Utf8   args
  #48 = Utf8   [Ljava/lang/String;
  #49 = Utf8   obj
  #50 = Utf8   Ljava/lang/Object;
  #51 = Utf8   StackMapTable
  #52 = Utf8   SourceFile
  #53 = Utf8   IntegerCompareTest.java
{
public com.jinwen.basetype.IntegerCompareTest();
flags: ACC_PUBLIC

Code:
  stack=1, locals=1, args_size=1
     0: aload_0       
     1: invokespecial #8                  // Method java/lang/Object."<init>":()V
     4: return        
  LineNumberTable:
    line 3: 0
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
           0       5     0  this   Lcom/jinwen/basetype/IntegerCompareTest;

 public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC

Code:
  stack=3, locals=2, args_size=1
     0: new           #16                 // class java/lang/Integer
     3: dup           
     4: sipush        1024
     7: invokespecial #18                 // Method java/lang/Integer."<init>":(I)V
    10: astore_1      
    11: aload_1       
    12: invokevirtual #21                 // Method java/lang/Object.toString:()Ljava/lang/String;
    15: invokestatic  #25                 // Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
    18: invokevirtual #29                 // Method java/lang/Integer.intValue:()I
    21: sipush        1024
    24: if_icmpne     35
    27: getstatic     #33                 // Field java/lang/System.out:Ljava/io/PrintStream;
    30: ldc           #39                 // String Integer.valueOf(obj.toString())==1024 is true
    32: invokevirtual #41                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    35: return        
  LineNumberTable:
    line 10: 0
    line 11: 11
    line 12: 27
    line 14: 35
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
           0      36     0  args   [Ljava/lang/String;
          11      25     1   obj   Ljava/lang/Object;
  StackMapTable: number_of_entries = 1
       frame_type = 252 /* append */
         offset_delta = 35
    locals = [ class java/lang/Object ]

}

分析:code的标号15:确实是调用静态的valueOf方法解析字符串为Integer实例,但其后标号18:有调用了Integer实例的intValue()方法返回了该实例相应的int值,从而最后比较的实际是int和int进行比较,所以比较结果是true;
通过上述分析可以确定 “包装对象==相应值” 这样的比较是可行的,但却不是推荐的。因为解析String实例为Integer实例,然后在去Integer实例里面去取的int值进行比较,在此Integer实例就多此一举了,还不如直接使用parseInt解析字符为相应的int值直接进行比较,节省创建一个Integer实例所浪费的资源。

#10. Math类的使用
public class HelloWorld {
public static void main(String[] args) {
int a=-5;
int b=128;
int c=12;
// 返回 int 值的绝对值。
int a1=Math.abs(a);
System.out.println(a1);
//返回两个 int 值中较大的一个。
System.out.println(Math.max(a,b));
//返回两个 int 值中较小的一个。
System.out.println(Math.min(a,b));
//返回第一个参数的第二个参数次幂的值
System.out.println(Math.pow(2,5));
//返回带正号的 double 值,该值大于等于 0.0 且小于 1.0。
System.out.println(Math.random());
//返回最接近参数的 int。(四舍五入)
System.out.println(Math.round(2.5));

}



}

#11. Random类的使用

public class HelloWorld {
public static void main(String[] args) {
Random rs=new Random();
System.out.println(rs.nextInt(10));
byte[] b=new byte[10];
rs.nextBytes(b);
for (int i:b
 ) {
System.out.println(i);
}
rs.setSeed(10);
System.out.println(rs.nextInt());
}
}

Random类 (java.util)

Random类中实现的随机算法是伪随机,也就是有规则的随机。在进行随机时,随机算法的起源数字称为种子数(seed),在种子数的基础上进行一定的变换,从而产生需要的随机数字。

相同种子数的Random对象,相同次数生成的随机数字是完全相同的。也就是说,两个种子数相同的Random对象,第一次生成的随机数字完全相同,第二次生成的随机数字也完全相同。这点在生成多个随机数字时需要特别注意。

#12. Runtime类的使用
Runtime类代表Java程序的运行时环境,每当一个JVM进程启动的时候都会存在一个Runtime对象,随着JVM的存在而存在的。但由于类Runtime的构造器是私有化的,所以不能实例化对象。Runtime类提供了一个静态方法getRuntime()返回一个Runtime对象。

public static Runtime getRuntime() {
return currentRuntime;
}

private Runtime() {}
然后可通过Runtime对象调用一些有趣的方法。

//返回 Java 虚拟机中的空闲内存量,供将来分配对象使用的当前可用内存的近似总量,以字节为单位。
public native long freeMemory();
//返回 Java 虚拟机中的内存总量,目前为当前和后续对象提供的内存总量,以字节为单位。
public native long totalMemory();
//返回 Java 虚拟机试图使用的最大内存量,如果内存本身没有限制,则返回值 Long.MAX_VALUE,以字节为单位。
public native long maxMemory();
// Java 虚拟机的可用的处理器数量。
public native int availableProcessors();
//执行指定的路径对应的可执行文件。
public Process exec(String command);
实例:

public class RuntimeExample {
public static void main(String[] args) throws IOException {
Runtime runtime = Runtime.getRuntime();
System.out.println("Java 虚拟机的可用的处理器数量: " + runtime.availableProcessors());
System.out.println("Java 虚拟机中的空闲内存量: " + runtime.freeMemory());
System.out.println("Java 虚拟机中的内存总量: " + runtime.totalMemory());
System.out.println("Java 虚拟机试图使用的最大内存量: " + runtime.maxMemory());

Process process = runtime.exec("C:\\Windows\\notepad.exe");//打开记事本程序,并返回一个进程
Thread.sleep(1000);//休眠一秒后关闭进程
process.destroy();
}
}

输出结果:

Java 虚拟机的可用的处理器数量: 8
Java 虚拟机中的空闲内存量: 188911352
Java 虚拟机中的内存总量: 192937984
Java 虚拟机试图使用的最大内存量: 2835349504

#13. System 类的使用
static PrintStream err
“标准”错误输出流。
static InputStream in
“标准”输入流。
static PrintStream out
“标准”输出流
public class HelloWorld {
public static void main(String[] args) {
int i=4;
String s=“asdasd”;
//返回与当前 Java 虚拟机关联的唯一 Console 对象(如果有)。
System.out.println(System.console());
//返回以毫秒为单位的当前时间。
System.out.println( System.currentTimeMillis());
//终止当前正在运行的 Java 虚拟机。
// System.exit(0);
//运行垃圾回收器。
System.gc();
//返回一个不能修改的当前系统环境的字符串映射视图。
Map<String,String> m=System.getenv();
System.out.println(m);
//确定当前的系统属性。
Properties p=System.getProperties();
System.out.println§;
//获取指定键指示的系统属性
String s1=System.getProperty(s);
System.out.println(s1);
//获取系统安全接口。
SecurityManager S=System.getSecurityManager();
System.out.println(S);

}

}

#14. Calendar类的使用
在实际项目当中,我们经常会涉及到对时间的处理,例如登陆网站,我们会看到网站首页显示XXX,欢迎您!今天是XXXX年。。。。某些网站会记录下用户登陆的时间,比如银行的一些网站,对于这些经常需要处理的问题,Java中提供了Calendar这个专门用于对日期进行操作的类,那么这个类有什么特殊的地方呢,首先我们来看Calendar的声明

public abstract class Calendar extends Objectimplements Serializable, Cloneable, Comparable
  该类被abstract所修饰,说明不能通过new的方式来获得实例,对此,Calendar提供了一个类方法getInstance,以获得此类型的一个通用的对象,getInstance方法返回一个Calendar对象(该对象为Calendar的子类对象),其日历字段已由当前日期和时间初始化:

Calendar rightNow = Calendar.getInstance();
  为什么说返回的是Calendar的子类对象呢,因为每个国家地区都有自己的一套日历算法,比如西方国家的第一个星期大部分为星期日,而中国则为星期一,我们来看看getInstance方法获取实例的源码
/**
* Gets a calendar using the default time zone and locale. The
* Calendar returned is based on the current time
* in the default time zone with the default locale.
*
* @return a Calendar.
*/
public static Calendar getInstance()
{
Calendar cal = createCalendar(TimeZone.getDefaultRef(), Locale.getDefault(Locale.Category.FORMAT));
cal.sharedZone = true;
return cal;
}

其中createCalendar方法就是根据不同国家地区返回对应的日期子类

private static Calendar createCalendar(TimeZone zone,
                                       Locale aLocale)
{
    Calendar cal = null;

    String caltype = aLocale.getUnicodeLocaleType("ca");
    if (caltype == null) {
        // Calendar type is not specified.
        // If the specified locale is a Thai locale,
        // returns a BuddhistCalendar instance.
        if ("th".equals(aLocale.getLanguage())
                && ("TH".equals(aLocale.getCountry()))) {
            cal = new BuddhistCalendar(zone, aLocale);
        } else {
            cal = new GregorianCalendar(zone, aLocale);
        }
    } else if (caltype.equals("japanese")) {
        cal = new JapaneseImperialCalendar(zone, aLocale);
    } else if (caltype.equals("buddhist")) {
        cal = new BuddhistCalendar(zone, aLocale);
    } else {
        // Unsupported calendar type.
        // Use Gregorian calendar as a fallback.
        cal = new GregorianCalendar(zone, aLocale);
    }

    return cal;
}

为了更加便捷的对日期进行操作,Calendar类对YEAR、MONTH、DAY_OF_MONTH、HOUR等日历字段之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法。瞬间可用毫秒值来表示,它是距历元(即格林威治标准时间 1970 年 1 月 1 日的 00:00:00.000,格里高利历)的偏移量。

下面看看Calendar常用的方法:

package com.test.calendar;

import java.util.Calendar;

import org.junit.Before;
import org.junit.Test;

public class CalendarDemo {
Calendar calendar = null;

@Before
public void test() {
    calendar = Calendar.getInstance();
}

// 基本用法,获取年月日时分秒星期
@Test
public void test1() {
    // 获取年
    int year = calendar.get(Calendar.YEAR);

    // 获取月,这里需要需要月份的范围为0~11,因此获取月份的时候需要+1才是当前月份值
    int month = calendar.get(Calendar.MONTH) + 1;

    // 获取日
    int day = calendar.get(Calendar.DAY_OF_MONTH);

    // 获取时
    int hour = calendar.get(Calendar.HOUR);
    // int hour = calendar.get(Calendar.HOUR_OF_DAY); // 24小时表示

    // 获取分
    int minute = calendar.get(Calendar.MINUTE);

    // 获取秒
    int second = calendar.get(Calendar.SECOND);

    // 星期,英语国家星期从星期日开始计算
    int weekday = calendar.get(Calendar.DAY_OF_WEEK);

    System.out.println("现在是" + year + "年" + month + "月" + day + "日" + hour
            + "时" + minute + "分" + second + "秒" + "星期" + weekday);
}

// 一年后的今天
@Test
public void test2() {
    // 同理换成下个月的今天calendar.add(Calendar.MONTH, 1);
    calendar.add(Calendar.YEAR, 1);

    // 获取年
    int year = calendar.get(Calendar.YEAR);

    // 获取月
    int month = calendar.get(Calendar.MONTH) + 1;

    // 获取日
    int day = calendar.get(Calendar.DAY_OF_MONTH);

    System.out.println("一年后的今天:" + year + "年" + month + "月" + day + "日");
}

// 获取任意一个月的最后一天
@Test
public void test3() {
    // 假设求6月的最后一天
    int currentMonth = 6;
    // 先求出7月份的第一天,实际中这里6为外部传递进来的currentMonth变量
    // 1
    calendar.set(calendar.get(Calendar.YEAR), currentMonth, 1);

    calendar.add(Calendar.DATE, -1);

    // 获取日
    int day = calendar.get(Calendar.DAY_OF_MONTH);

    System.out.println("6月份的最后一天为" + day + "号");
}

// 设置日期
@Test
public void test4() {
    calendar.set(Calendar.YEAR, 2000);
    System.out.println("现在是" + calendar.get(Calendar.YEAR) + "年");

    calendar.set(2008, 8, 8);
    // 获取年
    int year = calendar.get(Calendar.YEAR);

    // 获取月
    int month = calendar.get(Calendar.MONTH);

    // 获取日
    int day = calendar.get(Calendar.DAY_OF_MONTH);

    System.out.println("现在是" + year + "年" + month + "月" + day + "日");
}
}

Calendar类中也有before,after,compareTo等方法,用法与Date类的类似,只是现在推荐用Calendar类操作日期。

#15. Java8新特性
1. 简介
毫无疑问,Java 8是Java自Java 5(发布于2004年)之后的最重要的版本。这个版本包含语言、编译器、库、工具和JVM等方面的十多个新特性。在本文中我们将学习这些新特性,并用实际的例子说明在什么场景下适合使用。

这个教程包含Java开发者经常面对的几类问题:

语言
编译器
库
工具
运行时(JVM)
2. Java语言的新特性
Java 8是Java的一个重大版本,有人认为,虽然这些新特性领Java开发人员十分期待,但同时也需要花不少精力去学习。在这一小节中,我们将介绍Java 8的大部分新特性。

2.1 Lambda表达式和函数式接口
Lambda表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理:函数式开发者非常熟悉这些概念。很多JVM平台上的语言(Groovy、Scala等)从诞生之日就支持Lambda表达式,但是Java开发者没有选择,只能使用匿名内部类代替Lambda表达式。

Lambda的设计耗费了很多时间和很大的社区力量,最终找到一种折中的实现方案,可以实现简洁而紧凑的语言结构。最简单的Lambda表达式可由逗号分隔的参数列表、->符号和语句块组成,例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );
在上面这个代码中的参数e的类型是由编译器推理得出的,你也可以显式指定该参数的类型,例如:

Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );
如果Lambda表达式需要更复杂的语句块,则可以使用花括号将该语句块括起来,类似于Java中的函数体,例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> {
System.out.print( e );
System.out.print( e );
} );
Lambda表达式可以引用类成员和局部变量(会将这些变量隐式得转换成final的),例如下列两个代码块的效果完全相同:

String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach( 
( String e ) -> System.out.print( e + separator ) );
和

final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach( 
( String e ) -> System.out.print( e + separator ) );
Lambda表达式有返回值,返回值的类型也由编译器推理得出。如果Lambda表达式中的语句块只有一行,则可以不用使用return语句,下列两个代码片段效果相同:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
和

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
int result = e1.compareTo( e2 );
return result;
} );
Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式。java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了),举个简单的函数式接口的定义:

@FunctionalInterface
public interface Functional {
void method();
}
不过有一点需要注意,默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的。

@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
 
default void defaultMethod() {
}
}
Lambda表达式作为Java 8的最大卖点,它有潜力吸引更多的开发者加入到JVM平台,并在纯Java编程中使用函数式编程的概念。如果你需要了解更多Lambda表达式的细节,可以参考官方文档。

2.2 接口的默认方法和静态方法
Java 8使用两个新概念扩展了接口的含义:默认方法和静态方法。默认方法使得接口有点类似traits,不过要实现的目标不一样。默认方法使得开发者可以在 不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。

默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写,例子代码如下:

private interface Defaulable {
// Interfaces now allow default methods, the implementer may or 
// may not implement (override) them.
default String notRequired() { 
return "Default implementation"; 
}
}
 
private static class DefaultableImpl implements Defaulable {
}
 
private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return "Overridden implementation";
}
}
Defaulable接口使用关键字default定义了一个默认方法notRequired()。DefaultableImpl类实现了这个接口,同时默认继承了这个接口中的默认方法;OverridableImpl类也实现了这个接口,但覆写了该接口的默认方法,并提供了一个不同的实现。

Java 8带来的另一个有趣的特性是在接口中可以定义静态方法,例子代码如下:

private interface DefaulableFactory {
// Interfaces now allow static methods
static Defaulable create( Supplier< Defaulable > supplier ) {
return supplier.get();
}
}
下面的代码片段整合了默认方法和静态方法的使用场景:

public static void main( String[] args ) {
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
System.out.println( defaulable.notRequired() );
 
defaulable = DefaulableFactory.create( OverridableImpl::new );
System.out.println( defaulable.notRequired() );
}
这段代码的输出结果如下:

Default implementation
Overridden implementation
由于JVM上的默认方法的实现在字节码层面提供了支持,因此效率非常高。默认方法允许在不打破现有继承体系的基础上改进接口。该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等等。

尽管默认方法有这么多好处,但在实际开发中应该谨慎使用:在复杂的继承体系中,默认方法可能引起歧义和编译错误。如果你想了解更多细节,可以参考官方文档。

2.3 方法引用
方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。

西门的例子中,Car类是不同方法引用的例子,可以帮助读者区分四种类型的方法引用。

public static class Car {
public static Car create( final Supplier< Car > supplier ) {
return supplier.get();
}  
 
public static void collide( final Car car ) {
System.out.println( "Collided " + car.toString() );
}
 
public void follow( final Car another ) {
System.out.println( "Following the " + another.toString() );
}
 
public void repair() {   
System.out.println( "Repaired " + this.toString() );
}
}
第一种方法引用的类型是构造器引用,语法是Class::new,或者更一般的形式:Class<T>::new。注意:这个构造器没有参数。

final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
第二种方法引用的类型是静态方法引用,语法是Class::static_method。注意:这个方法接受一个Car类型的参数。

cars.forEach( Car::collide );
第三种方法引用的类型是某个类的成员方法的引用,语法是Class::method,注意,这个方法没有定义入参:

cars.forEach( Car::repair );
第四种方法引用的类型是某个实例对象的成员方法的引用,语法是instance::method。注意:这个方法接受一个Car类型的参数:

final Car police = Car.create( Car::new );
cars.forEach( police::follow );
运行上述例子,可以在控制台看到如下输出(Car实例可能不同):

Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
如果想了解和学习更详细的内容,可以参考官方文档

2.4 重复注解
自从Java 5中引入注解以来,这个特性开始变得非常流行,并在各个框架和项目中被广泛使用。不过,注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。Java 8打破了这个限制,引入了重复注解的概念,允许在同一个地方多次使用同一个注解。

在Java 8中使用@Repeatable注解定义重复注解,实际上,这并不是语言层面的改进,而是编译器做的一个trick,底层的技术仍然相同。可以利用下面的代码说明:

package com.javacodegeeks.java8.repeatable.annotations;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
public class RepeatingAnnotations {
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface Filters {
Filter[] value();
}
 
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( Filters.class )
public @interface Filter {
String value();
};
 
@Filter( "filter1" )
@Filter( "filter2" )
public interface Filterable {
}
 
public static void main(String[] args) {
for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
System.out.println( filter.value() );
}
}
}
正如我们所见,这里的Filter类使用@Repeatable(Filters.class)注解修饰,而Filters是存放Filter注解的容器,编译器尽量对开发者屏蔽这些细节。这样,Filterable接口可以用两个Filter注解注释(这里并没有提到任何关于Filters的信息)。

另外,反射API提供了一个新的方法:getAnnotationsByType(),可以返回某个类型的重复注解,例如Filterable.class.getAnnoation(Filters.class)将返回两个Filter实例,输出到控制台的内容如下所示:

filter1
filter2
如果你希望了解更多内容,可以参考官方文档。

2.5 更好的类型推断
Java 8编译器在类型推断方面有很大的提升,在很多场景下编译器可以推导出某个参数的数据类型,从而使得代码更为简洁。例子代码如下:

package com.javacodegeeks.java8.type.inference;
 
public class Value< T > {
public static< T > T defaultValue() { 
return null; 
}
 
public T getOrDefault( T value, T defaultValue ) {
return ( value != null ) ? value : defaultValue;
}
}
下列代码是Value<String>类型的应用:

package com.javacodegeeks.java8.type.inference;
 
public class TypeInference {
public static void main(String[] args) {
final Value< String > value = new Value<>();
value.getOrDefault( "22", Value.defaultValue() );
}
}
参数Value.defaultValue()的类型由编译器推导得出,不需要显式指明。在Java 7中这段代码会有编译错误,除非使用Value.<String>defaultValue()。

2.6 拓宽注解的应用场景
Java 8拓宽了注解的应用场景。现在,注解几乎可以使用在任何元素上:局部变量、接口类型、超类和接口实现类,甚至可以用在函数的异常定义上。下面是一些例子:

package com.javacodegeeks.java8.annotations;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
 
public class Annotations {
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
public @interface NonEmpty {
}
 
public static class Holder< @NonEmpty T > extends @NonEmpty Object {
public void method() throws @NonEmpty Exception {
}
}
 
@SuppressWarnings( "unused" )
public static void main(String[] args) {
final Holder< String > holder = new @NonEmpty Holder< String >();
@NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>();
}
}
ElementType.TYPE_USER和ElementType.TYPE_PARAMETER是Java 8新增的两个注解,用于描述注解的使用场景。Java 语言也做了对应的改变,以识别这些新增的注解。

3. Java编译器的新特性
3.1 参数名称
为了在运行时获得Java程序中方法的参数名称,老一辈的Java程序员必须使用不同方法,例如Paranamer liberary。Java 8终于将这个特性规范化,在语言层面(使用反射API和Parameter.getName()方法)和字节码层面(使用新的javac编译器以及-parameters参数)提供支持。

package com.javacodegeeks.java8.parameter.names;
 
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
 
public class ParameterNames {
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod( "main", String[].class );
for( final Parameter parameter: method.getParameters() ) {
System.out.println( "Parameter: " + parameter.getName() );
}
}
}
在Java 8中这个特性是默认关闭的,因此如果不带-parameters参数编译上述代码并运行,则会输出如下结果:

Parameter: arg0
如果带-parameters参数,则会输出如下结果(正确的结果):

Parameter: args
如果你使用Maven进行项目管理,则可以在maven-compiler-plugin编译器的配置项中配置-parameters参数:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerArgument>-parameters</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
4. Java官方库的新特性
Java 8增加了很多新的工具类(date/time类),并扩展了现存的工具类,以支持现代的并发编程、函数式编程等。

4.1 Optional
Java应用中最常见的bug就是空值异常。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便开发者写出更加整洁的代码。Java 8也将Optional加入了官方库。

Optional仅仅是一个容易:存放T类型的值或者null。它提供了一些有用的接口来避免显式的null检查,可以参考Java 8官方文档了解更多细节。

接下来看一点使用Optional的例子:可能为空的值或者某个类型的值:

Optional< String > fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) ); 
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
如果Optional实例持有一个非空值,则isPresent()方法返回true,否则返回false;orElseGet()方法,Optional实例持有null,则可以接受一个lambda表达式生成的默认值;map()方法可以将现有的Opetional实例的值转换成新的值;orElse()方法与orElseGet()方法类似,但是在持有null的时候返回传入的默认值。

上述代码的输出结果如下:

Full Name is set? false
Full Name: [none]
Hey Stranger!
再看下另一个简单的例子:

Optional< String > firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) ); 
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();
这个例子的输出是:

First Name is set? true
First Name: Tom
Hey Tom!
如果想了解更多的细节,请参考官方文档。

4.2 Streams
新增的Stream API(java.util.stream)将生成环境的函数式编程引入了Java库中。这是目前为止最大的一次对Java库的完善,以便开发者能够写出更加有效、更加简洁和紧凑的代码。

Steam API极大得简化了集合操作(后面我们会看到不止是集合),首先看下这个叫Task的类:

public class Streams  {
private enum Status {
OPEN, CLOSED
};
 
private static final class Task {
private final Status status;
private final Integer points;
 
Task( final Status status, final Integer points ) {
this.status = status;
this.points = points;
}
 
public Integer getPoints() {
return points;
}
 
public Status getStatus() {
return status;
}
 
@Override
public String toString() {
return String.format( "[%s, %d]", status, points );
}
}
}
Task类有一个分数(或伪复杂度)的概念,另外还有两种状态:OPEN或者CLOSED。现在假设有一个task集合:

final Collection< Task > tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 ) 
);
首先看一个问题:在这个task集合中一共有多少个OPEN状态的点?在Java 8之前,要解决这个问题,则需要使用foreach循环遍历task集合;但是在Java 8中可以利用steams解决:包括一系列元素的列表,并且支持顺序和并行处理。

// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
 
System.out.println( "Total points: " + totalPointsOfOpenTasks );
运行这个方法的控制台输出是:

Total points: 18
这里有很多知识点值得说。首先,tasks集合被转换成steam表示;其次,在steam上的filter操作会过滤掉所有CLOSED的task;第三,mapToInt操作基于每个task实例的Task::getPoints方法将task流转换成Integer集合;最后,通过sum方法计算总和,得出最后的结果。

在学习下一个例子之前,还需要记住一些steams(点此更多细节)的知识点。Steam之上的操作可分为中间操作和晚期操作。

中间操作会返回一个新的steam——执行一个中间操作(例如filter)并不会执行实际的过滤操作,而是创建一个新的steam,并将原steam中符合条件的元素放入新创建的steam。

晚期操作(例如forEach或者sum),会遍历steam并得出结果或者附带结果;在执行晚期操作之后,steam处理线已经处理完毕,就不能使用了。在几乎所有情况下,晚期操作都是立刻对steam进行遍历。

steam的另一个价值是创造性地支持并行处理(parallel processing)。对于上述的tasks集合,我们可以用下面的代码计算所有任务的点数之和:

// Calculate total points of all tasks
final double totalPoints = tasks
   .stream()
   .parallel()
   .map( task -> task.getPoints() ) // or map( Task::getPoints ) 
   .reduce( 0, Integer::sum );
 
System.out.println( "Total points (all tasks): " + totalPoints );
这里我们使用parallel方法并行处理所有的task,并使用reduce方法计算最终的结果。控制台输出如下:

Total points(all tasks): 26.0
对于一个集合,经常需要根据某些条件对其中的元素分组。利用steam提供的API可以很快完成这类任务,代码如下:

// Group tasks by their status
final Map< Status, List< Task > > map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
控制台的输出如下:

{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
最后一个关于tasks集合的例子问题是:如何计算集合中每个任务的点数在集合中所占的比重,具体处理的代码如下:

// Calculate the weight of each tasks (as percent of total points) 
final Collection< String > result = tasks
.stream()// Stream< String >
.mapToInt( Task::getPoints ) // IntStream
.asLongStream()  // LongStream
.mapToDouble( points -> points / totalPoints )   // DoubleStream
.boxed() // Stream< Double >
.mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
.mapToObj( percentage -> percentage + "%" )  // Stream< String> 
.collect( Collectors.toList() ); // List< String > 
 
System.out.println( result );
控制台输出结果如下:

[19%, 50%, 30%]
最后,正如之前所说,Steam API不仅可以作用于Java集合,传统的IO操作(从文件或者网络一行一行得读取数据)可以受益于steam处理,这里有一个小例子:

final Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}
Stream的方法onClose 返回一个等价的有额外句柄的Stream,当Stream的close()方法被调用的时候这个句柄会被执行。Stream API、Lambda表达式还有接口默认方法和静态方法支持的方法引用,是Java 8对软件开发的现代范式的响应。

4.3 Date/Time API(JSR 310)
Java 8引入了新的Date-Time API(JSR 310)来改进时间、日期的处理。时间和日期的管理一直是最令Java开发者痛苦的问题。java.util.Date和后来的java.util.Calendar一直没有解决这个问题(甚至令开发者更加迷茫)。

因为上面这些原因,诞生了第三方库Joda-Time,可以替代Java的时间管理API。Java 8中新的时间和日期管理API深受Joda-Time影响,并吸收了很多Joda-Time的精华。新的java.time包包含了所有关于日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。新设计的API认真考虑了这些类的不变性(从java.util.Calendar吸取的教训),如果某个实例需要修改,则返回一个新的对象。

我们接下来看看java.time包中的关键类和各自的使用例子。首先,Clock类使用时区来返回当前的纳秒时间和日期。Clock可以替代System.currentTimeMillis()和TimeZone.getDefault()。

// Get the system clock as UTC offset 
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
这个例子的输出结果是:

2014-04-12T15:19:29.282Z
1397315969360
第二,关注下LocalDate和LocalTime类。LocalDate仅仅包含ISO-8601日历系统中的日期部分;LocalTime则仅仅包含该日历系统中的时间部分。这两个类的对象都可以使用Clock对象构建得到。

// Get the local date and local time
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
 
System.out.println( date );
System.out.println( dateFromClock );
 
// Get the local date and local time
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
 
System.out.println( time );
System.out.println( timeFromClock );
上述例子的输出结果如下:

2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTime类包含了LocalDate和LocalTime的信息,但是不包含ISO-8601日历系统中的时区信息。这里有一些关于LocalDate和LocalTime的例子:

// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
 
System.out.println( datetime );
System.out.println( datetimeFromClock );
上述这个例子的输出结果如下:

2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
如果你需要特定时区的data/time信息,则可以使用ZoneDateTime,它保存有ISO-8601日期系统的日期和时间,而且有时区信息。下面是一些使用不同时区的例子:

// Get the zoned date/time
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
 
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );
这个例子的输出结果是:

2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]
最后看下Duration类,它持有的时间精确到秒和纳秒。这使得我们可以很容易得计算两个日期之间的不同,例子代码如下:

// Get duration between two dates
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );
 
final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );
这个例子用于计算2014年4月16日和2015年4月16日之间的天数和小时数,输出结果如下:

Duration in days: 365
Duration in hours: 8783
对于Java 8的新日期时间的总体印象还是比较积极的,一部分是因为Joda-Time的积极影响,另一部分是因为官方终于听取了开发人员的需求。如果希望了解更多细节,可以参考官方文档。

4.4 Nashorn JavaScript引擎
Java 8提供了新的Nashorn JavaScript引擎,使得我们可以在JVM上开发和运行JS应用。Nashorn JavaScript引擎是javax.script.ScriptEngine的另一个实现版本,这类Script引擎遵循相同的规则,允许Java和JavaScript交互使用,例子代码如下:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
 
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
这个代码的输出结果如下:

jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
4.5 Base64
对Base64编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行Base64编码,例子代码如下:

package com.javacodegeeks.java8.base64;
 
import java.nio.charset.StandardCharsets;
import java.util.Base64;
 
	public class Base64s {
public static void main(String[] args) {
final String text = "Base64 finally in Java 8!";
 
final String encoded = Base64
.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
 
final String decoded = new String( 
Base64.getDecoder().decode( encoded ),
StandardCharsets.UTF_8 );
System.out.println( decoded );
}
	}
这个例子的输出结果如下:

QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
新的Base64API也支持URL和MINE的编码解码。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。

4.6 并行数组
Java8版本新增了很多新的方法,用于支持并行数组处理。最重要的方法是parallelSort(),可以显著加快多核机器上的数组排序。下面的例子论证了parallexXxx系列的方法:

	package com.javacodegeeks.java8.parallel.arrays;
 
	import java.util.Arrays;
	import java.util.concurrent.ThreadLocalRandom;
 
	public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
 
Arrays.parallelSetAll( arrayOfLong, 
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
i -> System.out.print( i + " " ) );
System.out.println();
 
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
i -> System.out.print( i + " " ) );
System.out.println();
}
}
上述这些代码使用parallelSetAll()方法生成20000个随机数,然后使用parallelSort()方法进行排序。这个程序会输出乱序数组和排序数组的前10个元素。上述例子的代码输出的结果是:

Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378 
Sorted: 39 220 263 268 325 607 655 678 723 793
4.7 并发性
基于新增的lambda表达式和steam特性,为Java 8中为java.util.concurrent.ConcurrentHashMap类添加了新的方法来支持聚焦操作;另外,也为java.util.concurrentForkJoinPool类添加了新的方法来支持通用线程池操作(更多内容可以参考我们的并发编程课程)。

Java 8还添加了新的java.util.concurrent.locks.StampedLock类,用于支持基于容量的锁——该锁有三个模型用于支持读写操作(可以把这个锁当做是java.util.concurrent.locks.ReadWriteLock的替代者)。

在java.util.concurrent.atomic包中也新增了不少工具类,列举如下:

DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
5. 新的Java工具
Java 8提供了一些新的命令行工具,这部分会讲解一些对开发者最有用的工具。

5.1 Nashorn引擎:jjs
jjs是一个基于标准Nashorn引擎的命令行工具,可以接受js源码并执行。例如,我们写一个func.js文件,内容如下:

function f() { 
 return 1; 
}; 
 
print( f() + 1 );
可以在命令行中执行这个命令:jjs func.js,控制台输出结果是:

2
如果需要了解细节,可以参考官方文档。

5.2 类依赖分析器:jdeps
jdeps是一个相当棒的命令行工具,它可以展示包层级和类层级的Java类依赖关系,它以.class文件、目录或者Jar文件为输入,然后会把依赖关系输出到控制台。

我们可以利用jedps分析下Spring Framework库,为了让结果少一点,仅仅分析一个JAR文件:org.springframework.core-3.0.5.RELEASE.jar。

jdeps org.springframework.core-3.0.5.RELEASE.jar
这个命令会输出很多结果,我们仅看下其中的一部分:依赖关系按照包分组,如果在classpath上找不到依赖,则显示"not found".

org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
  -> java.io
  -> java.lang  
  -> java.lang.annotation   
  -> java.lang.ref  
  -> java.lang.reflect  
  -> java.util  
  -> java.util.concurrent   
  -> org.apache.commons.logging not found
  -> org.springframework.asmnot found
  -> org.springframework.asm.commonsnot found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
  -> java.lang  
  -> java.lang.annotation   
  -> java.lang.reflect  
  -> java.util
更多的细节可以参考官方文档。

6. JVM的新特性
使用Metaspace(JEP 122)代替持久代(PermGen space)。在JVM参数方面,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原来的-XX:PermSize和-XX:MaxPermSize。

7. 结论
通过为开发者提供很多能够提高生产力的特性,Java 8使得Java平台前进了一大步。现在还不太适合将Java 8应用在生产系统中,但是在之后的几个月中Java 8的应用率一定会逐步提高(PS:原文时间是2014年5月9日,现在在很多公司Java 8已经成为主流,我司由于体量太大,现在也在一点点上Java 8,虽然慢但是好歹在升级了)。作为开发者,现在应该学习一些Java 8的知识,为升级做好准备。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值