接下来主要从字节数、数据范围、默认值、以及用途等方面给大家总结成一个表格,一目了然。
数据类型 | 字节数 | 位数 | 最小值 | 最大值 | 默认值 | 用途 |
---|---|---|---|---|---|---|
byte | 1 | 8 | -128 | 127 | 0 | byte 类型用在大型数组中节约空间,主要代替整数。因为 byte 变量占用的空间只有 int 类型的四分之一 |
short | 2 | 16 | -32768 | 32767 | 0 | Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一 |
int | 4 | 32 | -2^31 | 2^31 - 1 | 0 | 一般地整型变量默认为 int 类型 |
long | 8 | 64 | -2^63 | 2^63-1 | 0L | 这种类型主要使用在需要比较大整数的系统上 |
float | 4 | 32 | 1.4e-45 | 3.4e38 | 0.0F | float 在储存大型浮点数组的时候可节省内存空间。浮点数不能用来表示精确的值,如货币 |
double | 8 | 64 | 4.9e-324 | 1.8e308 | 0.0D | 浮点数的默认类型为double类型。double类型同样不能表示精确的值,如货币 |
boolean | × | × | × | × | false | true和false |
char | 2 | 16 | \u0000 | \uffff | char 数据类型可以储存任何字符 |
上面提到的基本类型都有对应的包装类型,为了方便读者查看,我也整了一个表格。
基本类型 | 包装类型 | 最小值 | 最大值 |
---|---|---|---|
byte | Byte | Byte.MIN_VALUE |
Byte.MAX_VALUE |
short | Short | Short.MIN_VALUE |
Short.MAX_VALUE |
int | Integer | Integer.MIN_VALUE |
Integer.MAX_VALUE |
long | Long | Long.MIN_VALUE |
Long.MAX_VALUE |
float | Float | Float.MIN_VALUE |
Float.MAX_VALUE |
double | Double | Double.MIN_VALUE |
Double.MAX_VALUE |
boolean | Boolean | Boolean.MIN_VALUE |
Boolean.MAX_VALUE |
char | Char | Char.MIN_VALUE |
Char.MAX_VALUE |
在Java中,引用类型的变量非常类似于 C/C++ 的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Student、Dog 等。变量一旦声明后,类型就不能被改变了。
对象、数组都是引用数据类型。所有引用类型的默认值都是null。一个引用变量可以用来引用任何与之兼容的类型。例如:
person person = new person("张三")。
包装类型和基本类型之间如何转化呢?
Integer x = 2; // 装箱 调用了 Integer.valueOf(2)
int y = x; // 拆箱 调用了 X.intValue()
基本类型之间如何转换呢?有两个点:
- 把大容量的类型转换为小容量的类型时必须使用强制类型转换。
- 把小容量的类型转换为大容量的类型可以自动转换。
比如:
int i =128;
byte b = (byte)i;
long c = i;
大家思考一个问题:new Integer(123)
与 Integer.valueOf(123)
有什么区别?
有些人可能知道,有些人可能不知道。其实他们的区别很大。
- new Integer(123) 每次都会新建一个对象;
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
我写个demo大家就知道了
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
编译器会在自动装箱过程调用valueOf()
方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。如:
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
valueOf()
方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。我们看下源码就知道。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
根据数据类型的不一样,这个缓存池的上下限也不同,比如这个 Integer,就是 -128~127。不过这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
执行结果: true flase
解析:在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
}
输出结果为:false false
解析:在某个范围内的整型数值的个数是有限的,而浮点数却不是。
变量
public class Data{
static int aaa = 0; // 类变量(全局变量/静态变量/静态成员变量)
private String address; // 实例变量(成员变量/类属性)
public void add(){
int aa = 1; // 局部变量
}
}
成员变量是在对象创建以后存在于堆中,对象回收时,成员变量消失
局部变量是在方法被调用时存在于栈中,方法调执行结束,从栈中清除
局部变量:作用在方法内部
成员变量:作用于每个类
成员变量:随对象的创建而创建,对象回收时,成员变量消失
局部变量:随着方法的调用被创建,方法执行结束后,从栈中清除
Java 有一系列的关键字,在代码中各自有自己的重要用途与意义,今天就带着大家一起来了解一下 Java 的关键字!
Java 的关键字特别多,本文先主要介绍一下各个关键字的用途,然后重点介绍一下 final、static 和 this 这三个常用的关键字,其他的关键字大家用到的时候可以去网上查一下。
数据类型 | 含义 |
---|---|
abstract | 表明类或者成员方法具有抽象属性 |
assert | 断言,用来进行程序调试 |
boolean | 基本数据类型之一,布尔类型 |
break | 提前跳出一个块 |
byte | 基本数据类型之一,字节类型 |
case | 用在switch语句之中,表示其中的一个分支 |
catch | 用在异常处理中,用来捕捉异常 |
char | 基本数据类型之一,字符类型 |
class | 声明一个类 |
const | 保留关键字,没有具体含义 |
continue | 回到一个块的开始处 |
default | 默认,例如,用在switch语句中,表明一个默认的分支 |
do | 用在do-while循环结构中 |
double | 基本数据类型之一,双精度浮点数类型 |
else | 用在条件语句中,表明当条件不成立时的分支 |
enum | 枚举 |
extends | 表明一个类型是另一个类型的子类型,这里常见的类型有类和接口 |
final | 用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员域的值不能被改变,用来定义常量 |
finally | 用于处理异常情况,用来声明一个基本肯定会被执行到的语句块 |
float | 基本数据类型之一,单精度浮点数类型 |
for | 一种循环结构的引导词 |
goto | 保留关键字,没有具体含义 |
if | 条件语句的引导词 |
implements | 表明一个类实现了给定的接口 |
import | 表明要访问指定的类或包 |
instanceof | 用来测试一个对象是否是指定类型的实例对象 |
int | 基本数据类型之一,整数类型 |
interface | 接口 |
long | 基本数据类型之一,长整数类型 |
native | 用来声明一个方法是由与计算机相关的语言(如C/C++/FORTRAN语言)实现的 |
new | 用来创建新实例对象 |
package | 包 |
private | 一种访问控制方式:私用模式 |
protected | 一种访问控制方式:保护模式 |
public | 一种访问控制方式:共用模式 |
return | 从成员方法中返回数据 |
short | 基本数据类型之一,短整数类型 |
static | 表明具有静态属性 |
strictfp | 用来声明FP_strict(单精度或双精度浮点数)表达式遵循IEEE 754算术规范 [1] |
super | 表明当前对象的父类型的引用或者父类型的构造方法 |
switch | 分支语句结构的引导词 |
synchronized | 表明一段代码需要同步执行 |
this | 指向当前实例对象的引用 |
throw | 抛出一个异常 |
throws | 声明在当前定义的成员方法中所有需要抛出的异常 |
transient | 声明不用序列化的成员域 |
try | 尝试一个可能抛出异常的程序块 |
void | 声明当前成员方法没有返回值 |
volatile | 表明两个或者多个变量必须同步地发生变化 |
while | 用在循环结构中 |
是不是不列不知道,一列吓一跳,原来 Java 里还有这么多关键字,大部分我们平时都在用,只是没有特意去注意这个而已。所以大部分大家都很熟了,有些不常用的我也不总结了,我接下来主要总结几个比较有代表性的关键字。
Java 中的 final 关键字可以用来修饰类、方法和变量(包括实例变量和局部变量)
使用final修饰类则该类不能被继承,同时类中的所有成员方法都会被隐式定义为final方法(只有在需要确保类中的所有方法都不被重写时才使用final修饰类)。final修饰类的成员变量是可以更改的
public final class FinalClass{
int i = 1;
void test(){
System.out.println("FinalClass:test");
}
public static void main( String[] args ){
FinalClass ficl = new FinalClass();
System.out.println("ficl.i = " + ficl.i);
ficl.i = 2;
System.out.println("ficl.i = " + ficl.i);
}
}
使用final修饰方法可以将方法“锁定”,以防止任何继承类对方法的修改,也即使用final修饰方法,则子类无法重写(但并不影响继承和重载,即子类调用父类方法不受影响)。
使用final关键字修饰变量是使用最多的情况。
使用final修饰变量的值不能做再次更改,即不能重新赋值。
- 如果final修饰的变量是基本数据类型,则变量的值不可更改;
- 如果final修饰的变量是引用数据类型,则该变量不能再次指向其他引用(如重新指向新的对象或数组)但是该变量本身的内容可以再做修改(如数组本身的内容,或者对象的属性的修改)。
无论final修饰实例变量还是局部变量,都必须在使用前显式赋初值。
- Java中的实例变量系统会对其默认赋初值,但是局部变量必须先声明后赋值再使用。
- 虽然对于实例变量,系统会默认赋初值,但是Java仍然规定final修饰的实例变量必须显式赋初值。实例变量显式赋值的时机可以是在声明时直接赋值,也可以先声明,后在构造方法中赋值(对于含有多个构造方法,必须在每个构造方法中都显示赋值)。
我们来看个例子:
public void fun(){
//BufferedImage src = null;//0. 声明的同时赋值
BufferedImage src;//1. 这里不用赋初值,也不会出错
try{
src = ImageIO.read(new File("1.jpg"));//2.
} catch (Exception e){
//3. 如果出异常了就会进入这里,那么src可能无法被赋值
}
System.out.println(src.getHeight()); //4. src不一定有值,所以无法使用
}
如果静态变量同时被final修饰则可以将变量视为全局变量,即在整个类加载期间,其值不变。(static保证变量属于整个类,在类加载时只对其分配一次内存;final保证变量的值不被改变)
static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。
但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。也就是说,反过来是可以的。
如果说想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。static修饰成员方法最大的作用,就是可以使用"类名.方法名
"的方式调用方法,避免了new出对象的繁琐和资源消耗。
我们最常见的static方法就是main方法。至于为什么main方法必须是static的,这是因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。
static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
static成员变量的初始化顺序按照定义的顺序进行初始化。
static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。
所谓的代码块就是当我们初始化static修饰的成员时,可以将他们统一放在一个以static开始,用花括号包裹起来的块状语句中。例如:
class Person{
private Date birthDate;
public Person(Date birthDate){
this.birthDate = birthDate;
}
boolean isBornBoomer(){
Date startDate = Date.valueOf("1946");
Date endDate = Date.valueOf("1964");
return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
}
}
isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:
class Person{
private Date birthDate;
private static Date startDate,endDate;
static{
startDate = Date.valueOf("1946");
endDate = Date.valueOf("1964");
}
public Person(Date birthDate){
this.birthDate = birthDate;
}
boolean isBornBoomer(){
return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
}
}
将一些只需要进行一次的初始化操作都放在static代码块中进行。
this代表它所在函数所属对象的引用。简单说:哪个对象在调用this所在的函数,this就代表哪个对象。this关键字主要有以下三个作用:
- this调用本类中的属性,也就是类中的成员变量;
- this调用本类中的其他方法;
- this调用本类中的其他构造方法,调用时要放在构造方法的首行。(this语句只能定义在构造函数的第一行,因为在初始化时须先执行)
public class Person{
String name; //定义成员变量name
private void SetName(String name) { //定义一个参数(局部变量)name
this.name=name; //将局部变量的值传递给成员变量
}
}
虽然我们可以看明白这个代码的含义,但是作为Java编译器它是怎么判断的呢?到底是将形式参数name的值传递给成员变量name,还是反过来将成员变量name的值传递给形式参数name呢?也就是说,两个变量名字如果相同的话,那么Java如何判断使用哪个变量?此时this这个关键字就起到作用了。this这个关键字其代表的就是对象中的成员变量或者方法。也就是说,如果在某个变量前面加上一个this关键字,其指的就是这个对象的成员变量或者方法,而不是指成员方法的形式参数或者局部变量。
public class Person {
public Person(){ //无参构造器方法
this(“Hello!”);
}
public Person(String name){ //定义一个带形式参数的构造方法
}
}
在上述代码中,定义了两个构造方法,一个带参数,另一个没有带参数。在第一个没有带参数的构造方法中,使用了this(“Hello!”)这句代码,这句代码表示什么含义呢?在构造方法中使this关键字表示调用类中的构造方法。
如果一个类中有多个构造方法,因为其名字都相同,跟类名一致,那么这个this到底是调用哪个构造方法呢?其实,这跟采用其他方法引用构造方法一样,都是通过形式参数来调用构造方法的。
注意的是:利用this关键字来调用构造方法,只有在无参数构造方法中第一句使用this调用有参数的构造方法。否则的话,翻译的时候,就会有错误信息。这跟引用成员变量不同。如果引用成员变量的话,this关键字是没有位置上的限制的。
public HttpConfig url(String url) {
urls.set(url);
//return this就是返回当前对象的引用(就是实际调用这个方法的实例化对象)
return this;
}
return this就是返回当前对象的引用(就是实际调用这个方法的实例化对象),就像我们平时使用StringBuilder一样,可以一直.append()
,因为每次调用,返回的都是该对象的引用。
关于关键字,这篇文章就总结这么多,如有错误,欢迎指正,我们一起进步。
String不可变的含义是:将一个已有字符串"123"重新赋值成"456",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
也就是说:不可变的含义是内部数据不可变,而不是说引用不可变。
字符串对象可以使用“+”连接其他对象,其中字符串连接是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的,对象转换为字符串是通过 toString 方法实现的。可以通过反编译验证一下:
/**
* 测试代码
*/
public class Test {
public static void main(String[] args) {
int i = 10;
String s = "abc";
System.out.println(s + i);
}
}
/**
* 反编译后
*/
public class Test {
public static void main(String args[]) { //删除了默认构造函数和字节码
byte byte0 = 10;
String s = "abc";
System.out.println((new StringBuilder()).append(s).append(byte0).toString());
}
}
由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。那这个 “+” 的效率怎么样呢?
使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。比如:
String s = "abc";
for (int i=0; i<10000; i++) {
s += "abc";
}
这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。
与此之外还有一种特殊情况,也就是当"+"两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如:
System.out.println("Hello" + "World");
/**
* 反编译后
*/
System.out.println("HelloWorld");
JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。
/**
* 运行结果为true false
*/
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
由于常量池中不存在两个相同的对象,所以s1和s2都是指向JVM字符串常量池中的"abc"对象。new关键字一定会产生一个对象,并且这个对象存储在堆中。所以String s3 = new String(“abc”);
产生了两个对象:保存在栈中的s3和保存堆中的String对象。
StringBuilder 和 StringBuffer 的问题已经是老生常谈了,但是为什么还有那么多面试官喜欢问这个问题呢?
不过我也来把这个知识点给大家总结一下,先说说他们的相同点。
说到这单,我又忍不住要跟大家唠叨唠叨 String 了,我们先看下 String 的一个例子:
String a = "123";
a = "456";
// 打印出来的a为456
System.out.println(a)
不是说 String 是不可改变的吗?这怎么就变了呢?逗我吗?其实是没变的,原来那个 123 没变,相当于重新创建了一个 456,然后把 a 指向了这个 456,那个 123 会被回收掉。看看我给大家画的图:
而 StringBuffer 和 StringBuilder 就不一样了,他们是可变的,当一个对象被创建以后,通过append()
、insert()
、reverse()
、setCharAt()
、setLength()
等方法可以改变这个字符串对象的字符序列。也来看个例子:
StringBuffer b = new StringBuffer("123");
b.append("456");
// b打印结果为:123456
System.out.println(b);
OK,那现在我们知道了,StringBuffer 和 StringBuilder 有个共同点,那就是它们代表一个字符可变的字符串。那除此之外,还有没有其他相同点呢?
肯定是有的,比如:
super.append(...)
。StringBuffer 是线程安全的,StringBuilder 是非线程安全的。我们看源码都知道,StringBuffer 的方法上面都加了 synchronized 关键字,所以没有线程安全问题,但是性能会受影响,所以 StringBuilder 会比 StringBuffer 快,在单线程环境下,建议使用 StringBuilder。
另外还有个不同点是缓冲区。
先看下 StringBuilder 的toString()
方法:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
再看下 StringBuffer 的toString()
方法:
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
可以看出:StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。而 StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。
所以, StringBuffer 对缓存区优化,不过 StringBuffer 的这个toString 方法仍然是同步的。
OK,StringBuffer 和 StringBuilder 就总结这么多吧,如有问题,欢迎指正。
说到封装,Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。我们先来看下这三个修饰符的作用。
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例去代替,也就是确保满足里氏替换原则。
字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。
public class AccessExample {
public String id;
}
可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。
public class AccessExample {
private int id;
public String getId() {
return id + "";
}
public void setId(String id) {
this.id = Integer.valueOf(id);
}
}
继承是类与类的一种关系,是一种“is a”的关系。比如“狗”继承“动物”,这里动物类是狗类的父类或者基类,狗类是动物类的子类或者派生类。如下图所示:
注:java中的继承是单继承,即一个类只有一个父类。
基于上面两点,我们就知道实例化一个子类,java程序的执行顺序是:
父类对象属性初始化---->父类对象构造方法---->子类对象属性初始化—>子类对象构造方法
继承这块大家都比较熟,我来举个例子:
Animal动物:
public class Animal {
/**名称*/
public String name;
/**颜色*/
public String color;
/**显示信息*/
public void show(){
System.out.println("名称:"+name+",颜色:"+color);
}
}
Dog狗:
/**狗继承自动物,子类 is a 父类*/
public class Dog extends Animal {
/**价格*/
public double price;
}
dog并没有定义color属性,但在使用中可以调用,是因为dog继承了父类Animal,父类的非私有成员将被子类继承。如果再定义其它的动物类则无须再反复定义name,color与show方法。
多态这块,我要跟大家好好唠叨唠叨,首先来看下概念:
多态:一个对象具备多种形态。
①父类的引用类型变量指向了子类的对象
②接口的引用类型变量指向了接口实现类的对象
多态的前提:必须存在继承或者实现关系。
动物 a = new 狗();
总结:多态情况下,子父类存在同名的成员时,访问的都是父类的成员,除了在同名非静态函数时才是访问子类的。
注意:java编译器在编译的时候,会检查引用类型变量所属的类是否具备指定的成员,如果不具备马上编译报错。(编译看左边)
多态的好处: 提高了代码的拓 展性。
我们想想,平时写MVC三层模型的时候,service为什么要写接口呢?因为可能有个service会有多种不同的实现。service就是我们平时用多态最多的地方。
反射是框架设计的灵魂要素!
所谓的反射就是java语言在运行时拥有的一种自观的能力,反射使您的程序代码能够得到装载到JVM中的类的内部信息,允许您执行程序时才得到需要类的内部信息,而不是在编写代码的时候就必须要知道所需类的内部信息;也可以通俗的将这种动态获取信息以及动态调用对象的方法称为Java的反射机制.
通过Java的反射机制,程序猿们可以更深入的控制程序的运行过程,如在程序运行时对用户输入的信息进行验证,还可以逆向控制程序的执行过程,这也使反射成为构建灵活的应用的主要工具。
Java反射机制的实现要借助于4个类:
通过这四个对象我们可以粗略的看到一个类的各个组成部分。其中最核心的就是Class类,它是实现反射的基础,Class类包含的方法主要有:
序号 | 名称 | 功能 |
---|---|---|
1 | getName() | 获取此类型的全限定名 |
2 | getSuperClass() | 得到此类型的直接超类的全限定名 |
3 | isInterface() | 判断此类型是类类型还是接口类型 |
4 | getTypeParamters() | 获取这个类型的访问修饰符 |
5 | getInterfaces() | 获取任何直接超接口的全限定名的有序列表 |
6 | getFields() | 获取字段信息 |
7 | getMethods() | 获取方法信息 |
应用反射时我们最关心的一般是一个类的构造器、属性和方法,下面我们主要介绍Class类中针对这三个元素的方法:
语法 | 功能 |
---|---|
Constructor getConstructor(Class[] params) | 获得使用特殊的参数类型的公共构造函数 |
Constructor[] getConstructors() | 获得类的所有公共构造函数 |
Constructor getDeclaredConstructor(Class[] params) | 获得使用特定参数类型的构造函数(与接入级别无关) |
Constructor[] getDeclaredConstructors() | 获得类的所有构造函数(与接入级别无关) |
语法 | 功能 |
---|---|
Field getField(String name) | 获得命名的公共字段 |
Field[] getFields() | 获得类的所有公共字段 |
Field getDeclaredField(String name) | 获得类声明的命名的字段 |
Field[] getDeclaredFields() | 获得类声明的所有字段 |
语法 | 功能 |
---|---|
Method getMethod(String name, Class[] params) | 使用特定的参数类型,获得命名的公共方法 |
Method[] getMethods() | 获得类的所有公共方法 |
Method getDeclaredMethod(String name, Class[] params) | 使用特写的参数类型,获得类声明的命名的方法 |
Method[] getDeclaredMethods() | 获得类声明的所有方法 |
Class actionClass=Class.forName(“MyClass”);
Object action=actionClass.newInstance();
Method method = actionClass.getMethod(“myMethod”,null);
method.invoke(action,null);
上面就是最常见的反射使用的例子,前两行实现了类的装载、链接和初始化(newInstance方法实际上也是使用反射调用了
方法),后两行实现了从class对象中获取到method对象然后执行反射调用。下面简单分析一下该代码的具体原理。
通常有三种不同的方法:
1)Class c = Class.forName(“java.lang.String”)
2)Class c = MyClass.class
3)对于基本数据类型可以用Class c = int.class 或 Class c = Integer.TYPE这样的语句.
举个小栗子:先通过反射机制得到某个类的构造器,然后调用该构造器创建该类的一个实例
PS:反射的原理之一其实就是动态的生成类似于上述的字节码,加载到jvm中运行。
设想一下,上面的代码中,如果想要实现method.invoke(action,null)
调用action对象的myMethod
方法,只需要实现这样一个Method类即可:
Class Method{
public Object invoke(Object obj,Object[] param){
MyClass myClass=(MyClass)obj;
return myClass.myMethod();
}
}
首先来看一下Method对象是如何生成的:
- 使用Method m =myclass.getMethod("myMethod")获得了一个Class对象
- 接着对其进行判断,如果没有对应的cache,那么JVM就会为其创建一个并放入缓冲空间
- 处理器再判断Cache中是否存在"myMethod"
- 如果没有则返回NoSuchMethodException
- 如果存在那么就Copy一份"myMethod"对象并返回
上面的Class对象是在加载类时由JVM构造的,JVM为每个类管理一个独一无二的Class对象,这份Class对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象。每次getMethod获取到的Method对象都持有对根对象的引用,因为一些重量级的Method的成员变量(主要是MethodAccessor),我们不希望每次创建Method对象都要重新初始化,于是所有代表同一个方法的Method对象都共享着根对象的MethodAccessor,每一次创建都会调用根对象的copy方法复制一份:
Method copy() {
Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
res.root = this;
res.methodAccessor = methodAccessor;
return res;
}
获取到Method对象之后,调用invoke方法的流程如下:
m.invoke(obj,param);
- 首先调用MethodAccess.invoke
- 如果该方法的累计调用次数<=15,会创建出NativeMethodAccessorImp
- 如果该方法的累计调用次数>15,会由java代码创建出字节码组装而成的MethodAccessorImpl
我们可以看到,调用Method.invoke之后,会直接去调MethodAccessor.invoke
。MethodAccessor就是上面提到的所有同名method共享的一个实例,由ReflectionFactory创建。创建机制采用了一种名为inflation的方式(JDK1.4之后):如果该方法的累计调用次数<=15,会创建出NativeMethodAccessorImpl,它的实现就是直接调用native方法实现反射;如果该方法的累计调用次数>15,会由java代码创建出字节码组装而成的MethodAccessorImpl。(是否采用inflation和15这个数字都可以在jvm参数中调整)
以调用MyClass.myMethod(String s)
为例,生成出的MethodAccessorImpl字节码翻译成Java代码大致如下:
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public Object invoke(Object obj, Object[] args) throws Exception {
try {
MyClass target = (MyClass) obj;
String arg0 = (String) args[0];
target.myMethod(arg0);
} catch (Throwable t) {
throw new InvocationTargetException(t);
}
}
}
通过对java运行过程的详细分析,我们可以发现其中第1次和第16次调用是最耗时的(初始化NativeMethodAccessorImpl和字节码拼装MethodAccessorImpl)。初始化不可避免,因而native方式的初始化会更快,所以前几次的调用会采用native方法。
随着调用次数的增加,每次反射都使用JNI跨越native边界会对优化有阻碍作用,相对来说使用拼装出的字节码可以直接以Java调用的形式实现反射,发挥了JIT优化的作用,避免了JNI为了维护OopMap(HotSpot用来实现准确式GC的数据结构)进行封装/解封装的性能损耗。
在已经创建了MethodAccessor的情况下,使用Java版本的实现会比native版本更快。所以当调用次数到达一定次数(15次)后,会切换成Java实现的版本,来优化未来可能的更频繁的反射调用。
前面我们已经知道,Java 反射机制提供了一种动态链接程序组件的多功能方法,它允许程序创建和控制任何类的对象(根据安全性限制)之前,无需提前硬编码目标类。这些特性使得反射特别适用于创建以非常普通的方式与对象协作的库。例如,反射经常在持续存储对象为数据库、XML或其它外部格式的框架中使用。下面就已Hibernate框架为例像大家阐述一下反射的重要意义。
Hibernate是一个屏蔽了JDBC,实现了ORM的java框架,利用该框架我们可以抛弃掉繁琐的sql语句而是利用Hibernate中Session类的save()方法直接将某个类的对象存到数据库当中,也就是所涉及到sql语句的那些代码Hibernate帮我们做了。这时候就出现了一个问题,Hibernate怎样知道他要存的某个对象都有什么属性呢?这些属性都是什么类型呢?想一想,它在向数据库中存储该对象属性时的sql语句该怎么构造呢?OK,反射的作用此刻就体现出来了!
下面我们以一个例子来进行阐述,比如我们定义了一个User类,这个User类中有20个属性和这些属性的get和set方法,相应的在数据库中有一个User表,这个User表中对应着20个字段。假设我们从User表中提取了一条记录,现在需要将这条记录的20个字段的内容分别赋给一个User对象myUser的20个属性,而Hibernate框架在编译的时候并不知道这个User类,他无法直接调用myUser.getXXX或者myUser.setXXX方法,此时就用到了反射,具体处理过程如下:
根据查询条件构造PreparedStament语句,该语句返回20个字段的值;
Hibernate通过读取配置文件得到User类的属性列表list(是一个String数组)以及这些属性的类型;
创建myUser所属类的Class对象c;c = myUser.getClass();
构造一个for循环,循环的次数为list列表的长度;
读取list[i]的值,然后构造对应该属性的set方法;
判断list[i]的类型XXX,调用PreparedStament语句中的getXXX(i),进而得到i出字段的值;
将4.2中得到的值作为4.1中得到的set方法的参数,这样就完成了一个字段像一个属性的赋值,如此循环直至程序运行结束;
如果没有反射难以想象实现这么复杂的功能将会有多么难!
话说回来,反射给我们带来便利的同时也有它自身的缺点,比如性能较低、安全性较低、过程比较复杂等等,感兴趣的读者也可以在实际工作中再深入研究哦!
如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
按照异常需要处理的时机分为编译时异常(也叫强制性异常)也叫CheckedException 和运行时异常(也叫非强制性异常)也叫RuntimeException。
只有java 语言提供了Checked 异常,Java 认为 Checked 异常都是可以被处理的异常,所以Java 程序必须显式处理Checked 异常。如果程序没有处理Checked 异常,该程序在编译时就会发生错误无法编译。这体现了Java 的设计哲学:没有完善错误处理的代码根本没有机会被执行。对Checked 异常处理方法有两种:
运行时异常只有当代码在运行时才发行的异常,编译时不需要try catch。Runtime 如除数是0 和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。
那么,调用下面的方法,会得到什么结果呢?
public int getNum(){
try {
int a = 1/0;
return 1;
} catch (Exception e) {
return 2;
}finally{
return 3;
}
}
代码在走到第3 行的时候遇到了一个MathException,这时第四行的代码就不会执行了,代码直接跳转到catch语句中,走到第6 行的时候,异常机制有这么一个原则如果在catch 中遇到了return 或者异常等能使该函数终止的话那么有finally 就必须先执行完finally 代码块里面的代码然后再返回值。因此代码又跳到第8 行,可惜第8 行是一个return 语句,那么这个时候方法就结束了,因此第6 行的返回结果就无法被真正返回。如果finally 仅仅是处理了一个释放资源的操作,那么该道题最终返回的结果就是2。因此上面返回值是3。
Error 类和Exception 类的父类都是Throwable 类,他们的区别如下。
Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
Exception 类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception ),运行时异常;ArithmaticException,IllegalArgumentException,编译能通过,但是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。而受检查的异常,要么用try。。。catch 捕获,要么用throws 字句声明抛出,交给它的父类处理,否则编译不会通过。
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:
1. throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
2. throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
Java 对异常进行了分类,不同类型的异常分别用不同的Java 类表示,所有异常的根类为`java.lang.Throwable`,Throwable 下面又派生了两个子类:Error 和Exception,Error 表示应用程序本身无法克服和恢复的一种严重问题。
Exception 表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常,系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,
例如,数组脚本越界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常(ClassCastException);普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。
java 为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try..catch 处理或用throws 声明继续抛给上层调用方法处理,所以普通异常也称为checked 异常,而系统异常可以处理也可以不处理,所以,编译器不强制用try..catch 处理或用throws 声明,所以系统异常也称为unchecked 异常。
这是面试过程中,很喜欢问的问题,下面列举几个常见的RuntimeException。
1)java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
2)java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
3)java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
4)java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
5)java.lang.IllegalArgumentException 方法传递参数错误。
6)java.lang.ClassCastException 数据类型转换异常。
7)java.lang.NoClassDefFoundException 未找到类定义错误。
8)SQLException SQL 异常,常见于操作数据库时的SQL 语句错误。
9)java.lang.InstantiationException 实例化异常。
10)java.lang.NoSuchMethodException 方法不存在异常。
1)final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。
2)finally:异常处理语句结构的一部分,表示总是执行。
3)finalize:Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。
该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对象“死亡”,
这是一个被动的方法(其实就是回调方法),不需要我们调用。
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。
引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。
当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,
就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
Java IO:即 Java 输入 / 输出系统。
Java 的 IO 模型设计非常优秀,它使用Decorator
(装饰者)模式,按功能划分Stream
,您可以动态装配这些Stream
,以便获得您需要的功能。
Stream
:JAVA 中将数据的输入输出抽象为流,流是一组有顺序的、单向的,有起点和终点的数据集合。按照流中的最小数据单元又分为字节流和字符流。
IO 流用来处理设备之间的数据传输,Java 程序中,对于数据的输入/输出操作 都是以“流”的方式进行的。java.io
包下提供了各种“流”类的接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。
对于计算机来说,数据是以二进制形式读出或写入。我们可以把文件想象为一个桶,我们通过管道将桶里的水抽出来。这里的管道也就相当于Java中的流。流的本质是一种有序的数据集合,有数据源和目的地。
输入流:只能从内存中读数据
输出流:只能向文件中写数据
输入:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
输出:将程序(内存)数据输出到磁盘、光盘等存储设备中
. | 字节流 | 字符流 |
---|---|---|
输入流 | InputStream | Reader |
输出流 | OutputStream | Writer |
InputStream
:字节输入流OutputStream
:字节输出流Reader
:字符输入流Writer
:字符输出流在日常工作中,字节流一般用来处理图像,视频,以及PPT,Word类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,字节流可以用来处理纯文本文件,但是字符流不能用于处理图像视频等非文本类型的文件。
一个接口指的是Serializable.掌握了这些IO的核心操作那么对于Java中的IO体系也就有了一个初步的认识了。
Reader
Reader
:所有的输入字符流的父类,它是一个抽象类。CharReader
、StringReader
:两种基本的介质流,它们分别将Char 数组、String中读取数据。PipedReader
是从与其它线程共用的管道中读取数据。BufferedReader
:一个装饰器,它和其子类负责装饰其它Reader 对象。FilterReader
:所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,会增加一个行号。InputStreamReader
:一个连接字节流和字符流的桥梁,它将字节流转变为字符流。 FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream 转变为Reader 的方法。我们可以从这个类中得到一定的技巧。Reader 中各个类的用途和使用方法基本和InputStream 中的类使用一致。后面会有Reader 与InputStream 的对应关系。字节输入流InputStream主要方法:
read() :从此输入流中读取一个数据字节。
read(byte[] b) :从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。
read(byte[] b, int off, int len) :从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。
close():关闭此输入流并释放与该流关联的所有系统资源。
字节输出流OutputStream主要方法:
write(byte[] b) :将 b.length 个字节从指定 byte 数组写入此文件输出流中。
write(byte[] b, int off, int len) :将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流。
write(int b) :将指定字节写入此文件输出流。
close() :关闭此输入流并释放与该流关联的所有系统资源。
字符输入流Reader主要方法:
read():读取单个字符。
read(char[] cbuf) :将字符读入数组。
read(char[] cbuf, int off, int len) : 将字符读入数组的某一部分。
read(CharBuffer target) :试图将字符读入指定的字符缓冲区。
flush() :刷新该流的缓冲。
close() :关闭此流,但要先刷新它。
字符输出流Writer主要方法:
write(char[] cbuf) :写入字符数组。
write(char[] cbuf, int off, int len) :写入字符数组的某一部分。
write(int c) :写入单个字符。
write(String str) :写入字符串。
write(String str, int off, int len) :写入字符串的某一部分。
flush() :刷新该流的缓冲。
close() :关闭此流,但要先刷新它。
BufferedWriter类newLine() :写入一个行分隔符。这个方法会自动适配所在系统的行分隔符。
BufferedReader类readLine() :读取一个文本行。
字节流与字符流的转换主要用于文本文件在硬盘中以字节流的形式存储时,通过InputStreamReader读取后转化为字符流给程序处理,程序处理的字符流通过OutputStreamWriter转换为字节流保存。
转换流有哪些基本特点呢?
那么什么时候使用转换流呢?
具体的对象体现在哪些方面?
InputStreamReader
:字节到字符的桥梁OutputStreamWriter
:字符到字节的桥梁这两个流对象是字符体系中的成员,它们有转换作用,本身又是字符流,所以在构造的时候需要传入字节流对象进来。
OutputStreamWriter(OutStreamout)
:将字节流以字符流输出。InputStreamReader(InputStream in)
:将字节流以字符流输入。public class IOTest {
public static void write(File file) throws IOException {
// OutputStreamWriter可以显示指定字符集,否则使用默认字符集
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8");
// 要写入的字符串
String string = "松下问童子,言师采药去。只在此山中,云深不知处。";
osw.write(string);
osw.close();
}
public static String read(File file) throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
// 字符数组:一次读取多少个字符
char[] chars = new char[1024];
// 每次读取的字符数组先append到StringBuilder中
StringBuilder sb = new StringBuilder();
// 读取到的字符数组长度,为-1时表示没有数据
int length;
// 循环取数据
while ((length = isr.read(chars)) != -1) {
// 将读取的内容转换成字符串
sb.append(chars, 0, length);
}
// 关闭流
isr.close();
return sb.toString()
}
}
想要更系统的学习java IO系统,除了掌握这些基础的IO知识以外,重点是要学会IO模型,了解了各种IO模型之后才可以更好的理解java IO
注解,英文名Annotation。官方文档中对注解的定义是这样的:Java注解用于为Java代码提供元数据。作为元数据,注解不直接影响你的代码运行,但是也有一些类型的注解实际上可以用于这些目的。
Java注解是从 Java5 开始添加到 Java 里面的。看完这句话你可能对注解的定义还是一头雾水,接下来就和我一起结合案例来深入学习Java注解相关的知识吧。
日常开发中新建 Java 类,比较常见的有 class、interface 等,而注解同样也属于一种类,只不过它的修饰符是 ‘@interface’。
public interface Override extends Annotation {
}
一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不是。
元注解是用于修饰注解的注解,通常用在注解的定义上。
Java 中元注解有以下几种形式:
PS:@Target 用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法、修饰类亦或者是用来修饰字段属性的。
@Inherited
注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。/**自定义注解*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {
}
/**父类标注自定义注解*/
@MyTestAnnotation
public class Father {
}
/**子类*/
public class Son extends Father {
}
/**测试子类获取父类自定义注解*/
public class test {
public static void main(String[] args){
//获取Son的class对象
Class<Son> sonClass = Son.class;
// 获取Son类上的注解MyTestAnnotation可以执行成功
MyTestAnnotation annotation = sonClass.getAnnotation(MyTestAnnotation.class);
}
}
/**小Y喜欢玩游戏,他喜欢玩英雄联盟,绝地求生,极品飞车,尘埃4等,则我们需要定义一个人的注解,他属性代表喜欢玩游戏集合,一个游戏注解,游戏属性代表游戏名称*/
/**玩家注解*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface People {
Game[] value() ;
}
/**游戏注解*/
@Repeatable(People.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Game {
String value() default "";
}
/**玩游戏类*/
@Game(value = "LOL")
@Game(value = "PUBG")
@Game(value = "NFS")
@Game(value = "Dirt4")
public class PlayGame {
}
注解的属性其实和类中定义的变量有异曲同工之处,只是注解中的变量都是成员变量(属性),并且注解中是没有方法的,只有成员变量,变量名就是使用注解括号中对应的参数名,变量返回值注解括号中对应参数类型。相信这会你应该会对上面的例子有一个更深的认识。而@Repeatable
注解中的变量则类型则是对应 Annotation(接口)的泛型 Class。
/**注解Repeatable源码*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
/**
* Indicates the containing annotation type for the
* repeatable annotation type.
* @return the containing annotation type
*/
Class<? extends Annotation> value();
}
注解的本质就是一个Annotation接口。
/**Annotation接口源码*/
public interface Annotation {
boolean equals(Object obj);
int hashCode();
Class<? extends Annotation> annotationType();
}
从上述代码中我们可以看出,注解本身就是Annotation接口的子接口,也就是说注解中其实是可以有属性和方法,但是接口中的属性都是static final的,对于注解来说没什么意义,而我们定义接口的方法就相当于注解的属性,也就对应了前面说的为什么注解只有属性成员变量,其实他就是接口的方法,这就是为什么成员变量会有括号,不同于接口我们可以在注解的括号中给成员变量赋值。
如果注解又多个属性,则可以在注解括号中用“,”号隔开分别给对应的属性赋值,如下例子,注解在父类中赋值属性。
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {
String name() default "mao";
int age() default 18;
}
@MyTestAnnotation(name = "father",age = 50)
public class Father {
}
前面我们说了很多注解如何定义,放在哪,现在我们可以开始学习注解属性的提取了,这才是使用注解的关键,获取属性的值才是使用注解的目的。如果获取注解属性,当然是反射啦,主要有三个基本的方法:
/**是否存在对应 Annotation 对象*/
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return GenericDeclaration.super.isAnnotationPresent(annotationClass);
}
/**获取 Annotation 对象*/
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
Objects.requireNonNull(annotationClass);
return (A) annotationData().annotations.get(annotationClass);
}
/**获取所有 Annotation 对象数组*/
public Annotation[] getAnnotations() {
return AnnotationParser.toArray(annotationData().annotations);
}
下面结合前面的例子,我们来获取一下注解属性,在获取之前我们自定义的注解必须使用元注解@Retention(RetentionPolicy.RUNTIME)。
public class test {
public static void main(String[] args) throws NoSuchMethodException {
/**
* 获取类注解属性
*/
Class<Father> fatherClass = Father.class;
boolean annotationPresent = fatherClass.isAnnotationPresent(MyTestAnnotation.class);
if(annotationPresent){
MyTestAnnotation annotation = fatherClass.getAnnotation(MyTestAnnotation.class);
System.out.println(annotation.name());
System.out.println(annotation.age());
}
/**
* 获取方法注解属性
*/
try {
Field age = fatherClass.getDeclaredField("age");
boolean annotationPresent1 = age.isAnnotationPresent(Age.class);
if(annotationPresent1){
Age annotation = age.getAnnotation(Age.class);
System.out.println(annotation.value());
}
Method play = PlayGame.class.getDeclaredMethod("play");
if (play!=null){
People annotation2 = play.getAnnotation(People.class);
Game[] value = annotation2.value();
for (Game game : value) {
System.out.println(game.value());
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
注解 | 作用 |
---|---|
@SuppressWarnings | 对程序中的警告去除。 |
@Deprecated | 它是用于描述当前方法是一个过时的方法。 |
@Override | 主要是用来描述当前方法是一个重写的方法,在编译阶段对方法进行检查。jdk1.5中它只能描述继承中的重写,jdk1.6中它可以描述接口实现的重写,也能描述类的继承的重写。 |
如果你是一名Android 开发者,平常所使用的第三方框架ButterKnife,Retrofit2,Dagger2等都有注解的应用,如果想要了解这些框架的原理,则注解的基础知识则是必不可少的。
说再多不如举了例子来帮助大家理解:
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
Log.d("泛型测试","item = " + item);
}
这样的代码运行的最终结果必然是会崩溃的:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
List arrayList = new ArrayList();
...
//arrayList.add(100); 在编译阶段,编译器就会报错
那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现“java.lang.ClassCastException”异常呢?答案就是使用泛型。
E:元素(Element),多用于java集合框架。
K:关键字(Key)。
N:数字(Number)。
T:类型(Type)。
V:值(Value)。
泛型有三种使用方式,分别为:
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
定义一个泛型类:
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
}
将案例具体化:
public class Generic{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic genericInteger = new Generic(123456);
//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic genericString = new Generic("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());
测试结果:
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is 123456
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is key_vlaue
定义的泛型类并不是一定要传入泛型类型实参。在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
再举一个例子:
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());
运行结果:
D/泛型测试: key is 111111
D/泛型测试: key is 4444
D/泛型测试: key is 55.55
D/泛型测试: key is false
值得注意的是:
if(ex_num instanceof Generic){ }
定义一个泛型接口:public interface GenericIntercace{}
public interface GenericIntercace {
T getData();
}
实现泛型接口方式一:public class ImplGenericInterface1 implements GenericIntercace
public class ImplGenericInterface1 implements GenericIntercace {
private T data;
private void setData(T data) {
this.data = data;
}
@Override
public T getData() {
return data;
}
public static void main(String[] args) {
ImplGenericInterface1 implGenericInterface1 = new ImplGenericInterface1<>();
implGenericInterface1.setData("Generic Interface1");
System.out.println(implGenericInterface1.getData());
}
}
实现泛型接口方式二:public class ImplGenericInterface2 implements GenericIntercace {}
public class ImplGenericInterface2 implements GenericIntercace {
@Override
public String getData() {
return "Generic Interface2";
}
public static void main(String[] args) {
ImplGenericInterface2 implGenericInterface2 = new ImplGenericInterface2();
System.out.println(implGenericInterface2.getData());
}
}
泛型方法是在调用方法的时候指明泛型的具体类型 。
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public T genericMethod(Class tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
Object obj = genericMethod(Class.forName("com.test.test"));
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过
public class Generic{
private T key;
public Generic(T key) {
this.key = key;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public K showKeyName(Generic container){
* ...
* }
*/
public T showKeyName(Generic container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic这个泛型类做形参而已。
public void showKeyValue1(Generic obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public T showKeyName(Generic container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下
public class GenericFruit {
class Fruit{
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
class Person{
@Override
public String toString() {
return "Person";
}
}
class GenerateTest{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public void show_2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest generateTest = new GenerateTest();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
//generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static void show(T t){
}
}
至此,我们可以发现,在使用泛型类时,虽然有不同的泛型实参传入,但并没有真正意义上生成不同的类型,不同泛型实参的泛型类传入内存并只有一个,即还是原来的最基本的类型,当然,在逻辑上我们可以理解成多个不同的泛型类型。
细想原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
总结一下:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过Generic与Generic实际上是相同的一种基本类型。那么问题来了,在使用Generic作为形参的方法中,能否使用Generic的实例传入呢?在逻辑上类似于Generic和Generic是否可以看成具有父子关系的泛型类型呢?
弄清楚这个问题,使用Generic这个泛型类继续看下面的例子:
public void showKeyValue1(Generic obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic gInteger = new Generic(123);
Generic gNumber = new Generic(456);
showKeyValue(gNumber);
// showKeyValue这个方法编译器会为我们报错:Generic
// cannot be applied to Generic
// showKeyValue(gInteger);
在提示信息中,我们可以看到Generic不能被看作为`Generic的子类。
由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
再回到上面的例子,那么究竟如何解决上面的问题呢?
或许我们需要一个在逻辑上可以表示同时是Generic和Generic父类的引用类型。至此类型通配符应运而生。
我们可以将上面的方法改一下:
public void showKeyValue1(Generic obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
类型通配符一般是使用 ? 代替具体的类型实参,注意了,此处 ? 是类型实参,而不是类型形参 。
要说三遍!
此处 ? 是类型实参,而不是类型形参!
此处 ? 是类型实参,而不是类型形参!
再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
可以解决当具体类型不确定的时候,这个通配符就是 ? 。
当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
添加上边界,就是指传入的类型实参必须是指定类型的子类型。
public void showKeyValue1(Generic obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic generic1 = new Generic("s");
Generic generic2 = new Generic(1314520);
Generic generic3 = new Generic(11.11f);
Generic generic4 = new Generic(3.14);
//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);
showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);
如果再把把泛型类的定义也改一下:
public class Generic{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
//这一行代码也会报错,因为String不是Number的子类
Generic generic1 = new Generic("11111");
我们发现:泛型的上下边界添加,必须与泛型的声明在一起。
枚举是Java1.5引入的新特性,通过关键字enum来定义枚举类。枚举类是一种特殊类,它和普通类一样可以使用构造器、定义成员变量和方法,也能实现一个或多个接口,但枚举类不能继承其他类。
例如,你要指定一整个星期的天的枚举类型是:
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY
}
我们应该在需要使用固定组常量的任何时候使用枚举类型。这包括自然枚举类型,例如银河系的行星,这些你可以在编译时知道任何可能值。还有菜单选择,命令行标志等。
这里有一些介绍如何使用Day枚举类型声明的代码,如下:
public class EnumTest {
Day day;
public EnumTest(Day day) {
this.day = day;
}
public void tellItLikeItIs() {
switch (day) {
case MONDAY:
System.out.println("Mondays are bad.");
break;
case FRIDAY:
System.out.println("Fridays are better.");
break;
case SATURDAY: case SUNDAY:
System.out.println("Weekends are best.");
break;
default:
System.out.println("Midweek days are so-so.");
break;
}
}
public static void main(String[] args) {
EnumTest firstDay = new EnumTest(Day.MONDAY);
firstDay.tellItLikeItIs();
EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);
thirdDay.tellItLikeItIs();
EnumTest fifthDay = new EnumTest(Day.FRIDAY);
fifthDay.tellItLikeItIs();
EnumTest sixthDay = new EnumTest(Day.SATURDAY);
sixthDay.tellItLikeItIs();
EnumTest seventhDay = new EnumTest(Day.SUNDAY);
seventhDay.tellItLikeItIs();
}
}
上述代码输出为:
Mondays are bad.
Midweek days are so-so.
Fridays are better.
Weekends are best.
Weekends are best.
注意:任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔。
如果没有显式地声明基础类型的枚举,那么意味着它所对应的基础类型是 int。
Java 中的每一个枚举都继承自 java.lang.Enum 类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例,这些枚举成员默认都被 final、public, static 修饰,当使用枚举类型成员时,直接使用枚举名称调用成员即可。
方法名称 | 描述 |
---|---|
values() | 以数组形式返回枚举类型的所有成员 |
valueOf() | 将普通字符串转换为枚举实例 |
compareTo() | 比较两个枚举成员在定义时的顺序 |
ordinal() | 获取枚举成员的索引位置 |
Java编程语言枚举类型比其他编程语言更加强大。enum声明,定义了类(称为enum类型)。枚举类体,可以包含方法和其他字段。编译器为enum自动添加特殊的方法。例如,有一个静态的values方法,返回一个按照声明顺序排列的enum值数组。这个方法通常结合for-each结构,遍历enum类型的所有值。例如,下面Planet类里的代码,演示了遍历银河系的所有行星。
for (Planet p : Planet.values()) {
System.out.printf("Your weight on %s is %f%n",
p, p.surfaceWeight(mass));
}
所有enum类隐式继承java.lang.Enum。由于java不支持多继承,所有enum也不能继承其他类。
Planet是一个枚举类型,代表银河系里的所有行星。他们是恒定的质量和半径属性定义。
每个枚举常量都有质量和半径参数的声明。这些值,通过构造方法,在常量初始化时传递进来。java要求常量首先定义,其次才是字段和方法。所以,在字段和方法之前,enum常量列表必须以分号(;)结束。
注意:enum类型的构造方法必须是包内私有或者是private访问。它自动创建在enum体内的开始创建声明的常量,不允许直接调用enum的构造方法。
对于它的属性和构造方法,行星上有自己的方法,您可以检索每个行星的表面引力和重量。下面是一个示例程序,根据你在地球的体重(任何单位),计算并打印你在所有的行星的体重(相同单位):
public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7);
private final double mass; // in kilograms
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
private double mass() { return mass; }
private double radius() { return radius; }
// universal gravitational constant (m3 kg-1 s-2)
public static final double G = 6.67300E-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.err.println("Usage: java Planet ");
System.exit(-1);
}
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight/EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Your weight on %s is %f%n",
p, p.surfaceWeight(mass));
}
}
如果在命令行运行Planet.class,参数是175,输出是:
$ java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413
为了更好地支持枚举类型,java.util 中添加了两个新类:EnumMap 和 EnumSet。使用它们可以更高效地操作枚举类型。
EnumMap 类
EnumMap 是专门为枚举类型量身定做的 Map 实现。虽然使用其他的 Map(如 HashMap)实现也能完成枚举类型实例到值的映射,但是使用 EnumMap 会更加高效。
HashMap 只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以 EnumMap 使用数组来存放与枚举类型对应的值,使得 EnumMap 的效率非常高。
下面是使用 EnumMap 的一个代码示例。枚举类型 DataBaseType 里存放了现在支持的所有数据库类型。针对不同的数据库,一些数据库相关的方法需要返回不一样的值,例如示例中 getURL() 方法。
//定义数据库类型枚举
public enum DataBaseType
{
MYSQUORACLE,DB2,SQLSERVER
}
//某类中定义的获取数据库URL的方法以及EnumMap的声明
private EnumMapurls=new EnumMap(DataBaseType.class);
public DataBaseInfo()
{
urls.put(DataBaseType.DB2,"jdbc:db2://localhost:5000/sample");
urls.put(DataBaseType.MYSQL,"jdbc:mysql://localhost/mydb");
urls.put(DataBaseType.ORACLE,"jdbc:oracle:thin:@localhost:1521:sample");
urls.put(DataBaseType.SQLSERVER,"jdbc:microsoft:sqlserver://sql:1433;Database=mydb");
}
//根据不同的数据库类型,返回对应的URL
//@param type DataBaseType 枚举类新实例
//@return
public String getURL(DataBaseType type)
{
return this.urls.get(type);
}
在实际使用中,EnumMap对象urls往往是由外部负责整个应用初始化的代码来填充的。
从本例中可以看出,使用EnumMap可以很方便地为枚举类型在不同的环境中绑定到不同的值上。本例子中getURL绑定到URL上,在其他的地方可能又被绑定到数据库驱动上去。
EnumSet 类
EnumSet 是枚举类型的高性能 Set 实现,它要求放入它的枚举常量必须属于同一枚举类型。EnumSet 提供了许多工厂方法以便于初始化,如表 2 所示
方法名称 | 描述 |
---|---|
of(E first,e…rest) | 创建包含指定枚举成员的 EnumSet 对象 |
allOf(Class element type) | 创建一个包含指定枚举类型中所有枚举成员的 EnumSet 对象 |
range(E from ,E to) | 创建一个 EnumSet 对象,该对象包含了 from 到 to 之间的所有枚举成员 |
complementOf(EnumSet s) | 创建一个与指定 EnumSet 对象 s 相同的枚举类型 EnumSet 对象,并包含所有 s 中未包含的枚举成员 |
copyOf(EnumSet s) | 创建一个与指定 EnumSet 对象 s 相同的枚举类型 EnumSet 对象,并与 s 包含相同的枚举成员 |
noneOf(创建指定枚举类型的空 EnumSet 对象 |
|
枚举类型声明提供了一种用户友好的变量定义方法,枚举了某种数据类型所有可能出现的值。总结枚举类型,有以下特点:
Java 8是Java自Java 5(发布于2004年)之后的最重要的版本。这个版本包含语言、编译器、库、工具和JVM等方面的十多个新特性。在本文中我们将学习这些新特性,并用实际的例子说明在什么场景下适合使用。
本文中我们假设Java开发者经常面对的几类问题:
Lambda表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理:函数式开发者非常熟悉这些概念。如果没有lambda,Stream用起来相当别扭,他会产生大量的匿名内部类,所以lambda+default method使得jdk库更加强大,以及灵活,Stream以及集合框架的改进便是最好的证明。
(1). 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 ) );
(2). 当Lambda表达式需要更复杂的语句块时,可以使用花括号将该语句块括起来,类似于Java中的函数体,例如:
Arrays.asList( "a", "b", "d" ).forEach( e -> {
System.out.print( e );
System.out.print( e );
} );
(3). 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 ) );
(4). 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表达式。java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface,举个简单的函数式接口的定义:
@FunctionalInterface
public interface Functional {
void method();
}
如果你需要了解更多Lambda表达式的细节,一定要去看看官方文档哦。
方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。
举个例子:
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::new。注意:这个构造器没有参数。
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
方法二引用的类型是静态方法引用,语法是Class::static_method。注意:这个方法接受一个Car类型的参数。
cars.forEach( Car::collide );
方法三引用的类型是某个实例对象的成员方法的引用,语法是instance::method。注意:这个方法接受一个Car类型的参数:
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
方法四引用的类型是某个类的成员方法的引用,语法是Class::method,注意,这个方法没有定义入参:
cars.forEach( Car::repair );
运行上述例子,可以在控制台看到如下输出(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
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()等等。
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类型的应用:
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.defaultValue()。
自从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
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 语言也做了对应的改变,以识别这些新增的注解。
为了在运行时获得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参数:
org.apache.maven.plugins
maven-compiler-plugin
3.1
-parameters
1.8
1.8
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!
新增的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
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吸取的教训),如果某个实例需要修改,则返回一个新的对象。
方法 | 描述 |
---|---|
now() | 静态方法,根据当前时间创建对象 |
of() | 静态方法,根据指定日期/时间创建 对象 |
plusDays, plusWeeks, plusMonths, plusYears | 向当前 LocalDate 对象添加几天、 几周、几个月、几年 |
minusDays, minusWeeks, minusMonths, minusYears | 从当前 LocalDate 对象减去几天、 几周、几个月、几年 |
plus, minus | 添加或减少一个 Duration或 Period |
withDayOfMonth, withDayOfYear, withMonth, withYear | 将月份天数、年份天数、月份、年 份修改为指定的值并返回新的 LocalDate对象 |
getDayOfMonth | 获得月份天数(1-31) |
getDayOfYear | 获得年份天数(1-366) |
getDayOfWeek | 获得星期几(返回一个 DayOfWeek 枚举值) |
getMonth | 获得月份, 返回一个 Month枚举值 |
getMonthValue | 获得月份(1-12) |
getYear | 获得年份 |
until | 获得两个日期之间的 Period 对象, 或者指定 ChronoUnits的数字 |
isBefore, isAfter | 比较两个 LocalDate |
isLeapYear | 判断是否是闰年 |
我们接下来看看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
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
最后看下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的积极影响,另一部分是因为官方终于听取了开发人员的需求。
对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())。
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
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
基于新增的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
使用Metaspace(JEP 122)代替持久代(PermGen space)。在JVM参数方面,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原来的-XX:PermSize和-XX:MaxPermSize。
Map<Integer, List<SopEntity>> sopIdListMap =
sopEntities.stream().collect(Collectors.groupingBy(SopEntity::getId));
List<User> list = userList.stream()
.collect(Collectors.collectingAndThen(Collectors.toCollection(() ->
new TreeSet<>(Comparator.comparing(User::getUserId))), ArrayList::new));
List<user> result = list.stream().filter(item ->
item.getUserId!=null).collect(Collectors.toList());
dtoList.sort((m1, m2) -> m2.getPunchTime().compareTo(m1.getPunchTime()));
list.stream().sorted(Comparator.comparing(ServicePlan::getCreateTime).reversed())
int sumAge = pigs.stream().mapToInt(Pig::getAge).sum();
int maxAge = pigs.stream().mapToInt(Pig::getAge).max().getAsInt();
int minAge = pigs.stream().mapToInt(Pig::getAge).min().getAsInt();
double avgAge = pigs.stream().mapToInt(Pig::getAge).average().getAsDouble();
ist<MerchantManagementVo> result = resultList.stream()
.filter(item -> item.getManagerName()!=null &&
item.getManagerName().contains((CharSequence)
map.get("managerName"))).collect(Collectors.toList());
List<String> list = Stream.of(list1, list2)
.flatMap(Collection::stream).distinct().collect(Collectors.toList());
userList = list.stream().filter(item -> !(voList.stream().map(e -> e.getName())
.collect(Collectors.toList()).contains(item.getName())))
.collect(Collectors.toList());
线程私有,每个线程都会有自己独有的程序计数器。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期
与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都
会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信
息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、
float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始
地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress
类型(指向了一条字节码指令的地址)。
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机
栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)
方法服务。
本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规
定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接
就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失
败时分别抛出StackOverflowError和OutOfMemoryError异常。
1、Java虚拟机栈线程私有,生命周期与线程相同
2、Java虚拟机栈存放哪些东西?
a、方法运行时所需要的数据(栈帧)
b、局部变量表
c、操作栈、方法出口
3、局部变量表存放了什么?
a、编译期可知的基本数据类型、对象引用
1、所有线程共享
2、虚拟机启动时创建
3、存放实例对象
1、线程共享
2、存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存数据
3、运行时常量池
是方法区的一部分,具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,
并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,
这种特性被开发人员利用得比较多的便是String类的 intern()方法。
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中
定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现;
本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到
本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务
器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得
各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现
OutOfMemoryError异常。
详情请参照:https://zhuanlan.zhihu.com/p/440041666