抽象类:
1、包含一个抽象方法的类就是抽象类
2、声明而未被实现的方法,抽象方法必须使用abstract关键词字声明
3、抽象类被子类继承,子类(如果不是抽象类)必须重写抽象类中的所有抽象方法
4、抽象类不能被直接实例化,要通过其子类进行实例化
5、只要包含一个抽象方法的抽象类,该方法必须要定义成抽象类,不管是否还包含有其他方法。
6、子类中的抽象方法不能与父类的抽象方法同名。
7、abstract不能与final并列修饰同一个类。
8、abstract 不能与private、static、final或native并列修饰同一个方法。
接口:
1、接口是java中最重要的概念,接口可以理解为一种特殊的类,里面全部是由全局常量和公共的抽象方法组成
接口中所有方法都是抽象方法;
接口当中所有的方法都是public类型
2、一个接口不能继承抽象类,但可以通过extends关键字继承多个接口,实现接口的多继承
不同:
1、抽象类方式中,抽象类可以拥有任意范围的成员数据,同时也可以拥有自己的非抽象方法,
但是接口方式中,它仅能够有静态、不能修改的成员数据(但是我们一般是不会在接口中使用成员数据),同时它所有的方法都必须是抽象的。
2、在某种程度上来说,接口是抽象类的特殊化。
3、对子类而言,它只能继承一个抽象类(这是java为了数据安全而考虑的),但是却可以实现多个接口。
4、用抽象类来实现接口,可以不用重写接口的方法
static修饰的变量我们称之为静态变量,没有用static修饰的变量称之为实例变量,他们两者的区别是:
静态变量是随着类加载时被完成初始化的,它在内存中仅有一个,且JVM也只会为它分配一次内存,同时类所有的实例都共享静态变量,可以直接通过类名来访问它。
但是实例变量则不同,它是伴随着实例的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。
所以我们一般在这两种情况下使用静态变量:对象之间共享数据、访问方便。
static修饰的方法我们称之为静态方法,我们通过类名对其进行直接调用。由于他在类加载的时候就存在了,它不依赖于任何实例,所以static方法必须实现,也就是说他不能是抽象方法abstract。
static方法是类中的一种特殊方法,我们只有在真正需要他们的时候才会将方法声明为static。如Math类的所有方法都是静态static的。
被static修饰的代码块,我们称之为静态代码块,静态代码块会随着类的加载一块执行,而且他可以随意放,可以存在于该了的任何地方。
Static确实是存在诸多的作用,但是它也存在一些缺陷。
1、它只能调用static变量。
2、它只能调用static方法。
3、不能以任何形式引用this、super。
4、static变量在定义时必须要进行初始化,且初始化时间要早于非静态变量。
1、修饰类当用final去修饰一个类的时候,表示这个类不能被继承。
注意:
a. 被final修饰的类,final类中的成员变量可以根据自己的实际需要设计为final。
b. final类中的成员方法都会被隐式的指定为final方法。
2、修饰方法
被final修饰的方法不能被重写。
注意:
a. 一个类的private方法会隐式的被指定为final方法。
b. 如果父类中有final修饰的方法,那么子类不能去重写。
3、修饰成员变量
注意:
a. 必须要赋初始值,而且是只能初始化一次。
1、抽象类是为了把相同的但不确定的东西的提取出来,为了以后的重用。定义成抽象类的目的,就是为了在子类中实现抽象类。
2、用abstract修饰的类,即抽象类;用abstract修饰的方法,即抽象方法。
3、抽象方法不能有方法主体。
4、抽象类不能被实例化。
5、抽象类中不一定要包含abstract方法。也就是了,抽象中可以没有abstract方法。
6、一旦类中包含了abstract方法,那类该类必须声明为abstract类。
poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
答案应该是:两个或一个。
1、如果abc 字符串之前没有用过,这毫无疑问创建了两个对象,一个是new String 创建的一个新的对象,一个是常量“abc”对象的内容创建出的一个新的String对象。
2、如果是:
String str1 = "abc";
String str2 = new String("abc");
那么就是1个。
强引用就是指在程序代码之中普遍存在的
Object object = new Object();
String str = "hello";
只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
比如Vector类的clear方法中就是通过将引用赋值为null来实现清理工作的。
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
如何利用软引用和弱引用解决OOM问题:
假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路是:用一个HashMap来保存图片的路径 和 相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率,但是 WeakReference ,一旦失去最后一个强引用,就会被 GC 回收,而软引用虽然不能阻止被回收,但是可以延迟到 JVM 内存不足的时候。
WeakHashMap 是怎么工作的?
WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(new String("hello"), queue);
System.out.println(pr.get());
}
}
当你从一个构造器中调用另一个构造器,就是Java 中的构造器链。这种情况只在重载了类的构造器的时候才会出现。
是的,我们可以做强制转换,但是 Java 中 int 是 32 位的,而 byte 是 8 位的,所以,如果强制转化是,int 类型的高 24 位将会被丢弃,byte 类型的范围是从 -128 到 128。
基本数据类型,转换规则还可以通过类型本身空间大小和精度分析明白,而且最多就是丢失精度但运行起来至少是不会报错。
可是面对引用数据类型,这个“坑”就大了:有自动转的,有强制转的,居然还有强制都转不了的;自动转了的却把对象身上的方法丢了看不见;强制转的编译过了运行却可能报异常。
java.lang.Cloneable 是一个标示性接口,不包含任何方法,clone 方法在 object 类中定义。并且需要知道 clone() 方法是一个本地方法,这意味着它是由 c 或 c++ 或 其他本地语言实现的。
可以,向下转型。但是不建议使用,容易出现类型转型异常。
instanceof
instanceof是一个二元操作符,用法是:boolean result = a instanceof ClassA,即判断对象a是否是类ClassA的实例,如果是的话,则返回true,否则返回false。
向上转型
Java中父类的引用指向子类对象是自动成功的。
Peg dog = new Dog();
父类引用指向子类对象,只能调用到来自父类的属性/行为。
向下转型
Peg dog = new Dog();
Dog dog2 = (Dog)dog;
如果向下转型想要编译和运行都成功,必须使用强制转型语法,还必须要求运行起来父类引用确实指向该子类的对象。所以,为了降低这种风险性,我们可以使用Java中的instance运算符,在强转前进行一次判断。
不是线程安全的操作。它涉及到多个指令,如:
1、读取变量值;
2、增加;
3、然后存储回内存。
这个过程可能会出现多个线程交差。
+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两这个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。如果加法操作的结果比 a 的最大值要大,则 a+b 会出现编译错误,但是 a += b 没问题,如下:
byte a = 127;
byte b = 127;
b = a + b; // error : cannot convert from int to byte
其实无论 a+b 的值为多少,编译器都会报错,因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错。
b += a; // ok
从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。
Java 中的 String 不可变是因为 Java 的设计者认为字符串使用非常频繁,将字符串设置为不可变可以允许多个客户端之间共享相同的字符串。
StringBuilder和StringBuffer的区别:
在这方面运行速度快慢为:StringBuilder > StringBuffer > String
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的。
StringBuffer为什么是线程安全:直接通过synchronized 关键字来实现同步操作。
数组是对象哦。数组的父类也是Object,每个数组都实现了接口Cloneable and java.io.Serializable。但是数组的类型却不是程序员可见的。但是由于数组也是Object的子类,我们可以打印出数据的类型名。
首先子类是无法继承父类的私有属性,也无法直接访问父类的私有属性。
但如果父类中有对私有属性的get和set的方法,而且是public的修饰的方法,
子类在继承父类的同时,也继承了带有public修饰的set和get方法,所以可以通过以下方式子类可以访问到父类的私有属性。
1、子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。
2、子类构造器执行体的第一行使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。
3、子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
this
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this的用法在java中大体可以分为3种:
1.普通的直接引用
这种就不用讲了,this相当于是指向当前对象本身。
2.形参与成员名字重名,用this来区分:
3.引用构造函数
这个和super放在一起讲,见下面。
super
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super也有三种用法:
1.普通的直接引用
与this类似,super相当于是指向当前对象的父类,这样就可以用super.xxx来引用父类的成员。
2.子类中的成员变量或方法与父类中的成员变量或方法同名
3.引用构造函数
super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
在java语言中,错误类的基类是java.lang.Error,异常类的基类是java.lang.Exception。
1)相同点:java.lang.Error和java.lang.Exception都是java.lang.Throwable的子类,因此java.lang.Error和java.lang.Exception自身及其子类都可以作为throw的使用对象,如:throw new MyError();和throw new MyException();其中,MyError类是java.lang.Error的子类,MyException类是java.lang.Exception的子类。
2)不同点:java.lang.Error自身及其子类不需要try-catch语句的支持,可在任何时候将返回方法。
通过File类。
1、通过synchronized 关键字给方法加上内置锁来实现线程安全
Timer,TimerTask,Vector,Stack,HashTable,StringBuffer
2、原子类Atomicxxx—包装类的线程安全类
如AtomicLong,AtomicInteger等等
Atomicxxx 是通过Unsafe 类的native方法实现线程安全的
3、CopyOnWriteArrayList和 CopyOnWriteArraySet
4、ConcurrentHashMap,当然还有ConcurrentSkipListSet和ConcurrentSkipListMap等等。
5、ThreadPoolExecutor
ThreadPoolExecutor也是使用了ReentrantLock显式加锁同步
Integer 对象会占用更多的内存。Integer 是一个对象,需要存储对象的元数据。但是 int 是一个原始类型的数据,所以占用的空间更少。
不行,你不能在没有强制类型转换的前提下将一个 double 值赋值给 long 类型的变量,因为 double 类型的范围比 long 类型更广,所以必须要进行强制转换。
1、对象锁也叫实例锁,对应synchronized关键字,当多个线程访问多个实例时,它们互不干扰,每个对象都拥有自己的锁,如果是单例模式下,那么就是变成和类锁一样的功能。
2、对象锁防止在同一个时刻多个线程访问同一个对象的synchronized块。如果不是同一个对象就没有这样子的限制。
3、类锁对应的关键字是static sychronized,是一个全局锁,无论多少个对象都共享同一个锁(也可以锁定在该类的class上或者是classloader对象上),同样是保障同一个时刻多个线程同时访问同一个synchronized块,当一个线程在访问时,其他的线程等待。
一个类实现单例模式时需要具备以下3个条件:
1)类的内部定义一个该类的静态私有成员变量;
2)构造方法为私有;
3)提供静态工厂方法,供外部获取类的实例;
1)懒汉式单例
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2)饿汉式单例
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
3)双重检测单例
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
JVM在创建一个对象时会进行以下步骤:
1)分配对象内存空间;
2)初始化对象;
3)设置instance指向分配的内存地址;
4)静态内部类单例
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonFactory.instance;
}
static class SingletonFactory {
private final static Singleton instance = new Singleton();
}
}
静态内部类单例模式是一种比较优秀的实现方式,也是《Effective Java》书中推荐的方式。一方面,使用延时加载,使用时才进行对象初始化,也不会造成造成资源浪费;另一方面,由于JVM在类的加载时已经做了同步处理,不会出现线程安全问题。
在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。
成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:
class Circle {
double radius = 0;
public Circle(double radius) {
this.radius = radius;
}
class Draw { //内部类
public void drawSahpe() {
System.out.println("drawshape");
}
}
}
1、这样看起来,类Draw像是类Circle的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
2、不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
外部类.this.成员变量
外部类.this.成员方法
3、虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问。
4、成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。
public class Test {
public static void main(String[] args) {
//第一种方式:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建
//第二种方式:
Outter.Inner inner1 = outter.getInnerInstance();
}
}
5、内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。
Animal an=new Animal(){
public void bark(){
System.out.println("汪汪汪...");
}
};
an.bark();
1.通过实体类创建匿名内部类对象//相当于创建该类的一个子类对象
2.通过抽象类创建匿名内部类对象//相当于定义了该抽象类的一个子类对象,并重写了抽象类中所有的抽象方法
3.通过接口创建匿名内部类对象//相当于定义了该接口的一个实现类对象,并重写了接口中所有的抽象方法
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法。
是的,我们是可以创建一个包含可变对象的不可变对象的,你只需要谨慎一点,不要共享可变对象的引用就可以了,如果需要变化时,就返回原对象的一个拷贝。最常见的例子就是对象中包含一个日期对象的引用。
可以使用 String 接收 byte[] 参数的构造器来进行转换,需要注意的点是要使用的正确的编码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。
使用new关键字 |
} → 调用了构造函数 |
使用Class类的newInstance方法 |
} → 调用了构造函数 |
使用Constructor类的newInstance方法 |
} → 调用了构造函数 |
使用clone方法 |
} → 没有调用构造函数 |
使用反序列化 |
2.0-1.1 = 0.8999999999999999
1.0-0.1 = 0.9
结论如下:
1.在Java中,未声明类型的小数会被默认当做double型处理。
2.double浮点数在计算机中的存储是二进制(不能除尽)。
解析:
double类型占了64位。
1、第一位为符号位,0为正,1为负。
2、最后52位为十进制转换为二进制后的值。
3、中间11位为阶码,可表示-1024~1023,表示最后52位为2的多少次方。比如最后52位为二进制的1.0*2^1(即二进制10),那么十一位阶码的十进制值为1,为了方便表示,加上1023变成非负数。
float类型占32位(1位符号位,8位阶码,23位二进制值)
将2.0转换为二进制表示
整数部分为10
小数部分为00000000000000000000000000000000000000000000000000
整数部分和小数部分合起来占52位
将1.1转换为二进制表示
整数部分为1 (整数部分转换为二进制为不停地除以2,直到为1为止)
小数部分为00011001100110011001100110011001100110011001100110 (0.1转换为二进制表示为不停地*2,直到小数部分为0为止)
最终二进制2.0 减去二进制1.1 (原理同十进制减法,不够向前借位)
得到0.111001100110011001100无穷循环,为0.89999999
2.0二进制为:0 10000000000 1000000000000000000000000000000000000(51个0) (阶位计算为1+1023)
1.1二进制位:0 01111111111 100011001100110011001100110011001100 1100 (阶位计算为0+1023)
0.89999999表示为二进制为:0 011111110 111001100110011001100(1100循环直到52位) (阶位计算为-1 + 1023)
●Java 语言支持两种基本的浮点类型: float 和 double。
●float是单精度类型,精度是8位有效数字,取值范围是10的-38次方到10的38次方,float占用4个字节的存储空间
●double是双精度类型,精度是17位有效数字,取值范围是10的-308次方到10的308次方,double占用8个字节的存储空间
●另将double型数据转换为float型数据时,会有一定程度上的精度丢失。
● BigDecimal是不可变的,可表示任意精度的有符号的十进制数字。
● 对所有的运算,BigDecimal计算操作会先返回一个精确的中间结果,然后根据设置的精度要求进行数据精度取舍(比如四舍五入),否则默认使用首选精度。除了一个逻辑精确的结果,加减乘除每种运算操作都具有表示结果的首选精度范围。(加减乘除首选精度以下图表):
● float、double虽然经过BigDecimal封装,但是运算结果同样存在误差,那么怎么才能保证运算的准确行呢?通过一些实际使用与试验验证的经验,推荐方法是使用BigDecimal构造函数BigDecimal(String),将基本数据类型float/double型数据转成String,然后通过String创建BigDecimal实例进行运算。
为什么char类型数组可以直接用数组名打印,其它类型数组是地址值?
char类型的数组就相当于一个字符串。
因为输出流System.out是PrintStream对象,PrintStream有多个重载的println方法,其中一个就是public void println(char[] x),直接打印字符数组的话,不像int[]等其他数组,它会直接调用这个方法来打印,因而可以打印出数组内容,而不是地址。
char[] cArr ={'北', '京'};
System.out.println(cArr); 北京
System.out.println(Arrays.toString(cArr));[北, 京]
父/子类执行步骤:
1、父类静态变量和静态代码块(先声明的先执行);
2、子类静态变量和静态代码块(先声明的先执行);
3、父类的变量和代码块(先声明的先执行);
4、父类的构造函数;
5、子类的变量和代码块(先声明的先执行);
6、子类的构造函数。
规律一、初始化构造时,先父后子;只有在父类所有都构造完后子类才被初始化
规律二、类加载先是静态、后非静态、最后是构造函数
规律三、java中的类只有在被用到的时候才会被加载
规律四、java类只有在类字节码被加载后才可以被构造成对象实例
关于内部类:
加载一个类时,其内部类不会同时被加载。
一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
new对象/反射/类对象静态域执行时包括:java中的类加载和初始化(类变量初始化)。
1、每个方法中的局部变量所需的内存大小是在编译期确定的。
2、方法都是在栈中运行所以调用时候分配,在方法运行时会创建一个栈帧用于存储局部变量。
3、局部变量在局部范围使用时创建,跳出局部范围销毁。不是回收,而只是,栈指针增增减减,栈里面的内容会被不断覆盖。
自动装箱,赋值操作,在内存里面是如何实现的。
Integer i = 10; //装箱int n = i; //拆箱
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
在自动装箱时对于值从–128到127之间的值,它们被装箱为Integer对象后,会存在内存中被重用,始终只存在一个对象 。而如果超过了从–128到127之间的值,被装箱后的Integer对象并不会被重用,即相当于每次装箱时都新建一个 Integer对象。
out
request
response
session
pageContext
application
config
page
exception
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
内存泄漏是内存溢出的一种原因。
内存泄漏的测试:
return elements[--size];
改成:
Object result = elements[--size];
elements[size] = null;
return result;
1、Lambda 表达式,也可称为闭包,本质只是一个"语法糖",由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能。
2、Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
3、lambda表达式允许你通过表达式来代替功能接口。
4、lambda和java内部类的特性有点相似,匿名内部类不只是一个方法,而是一个包含一个或多个方法的类,他们的作用都是一样的,都是作为方法的参数传递。
5、当lambda表达式作为参数传递给方法时,接收方法把它当对象使用。
6、lambda可以表示函数式接口(只有一个抽象方法)。
(parameters) -> expression
或
(parameters) ->{ statements; }
以下是lambda表达式的重要特征:
可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
使用 Lambda 表达式需要注意以下两点:
Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
1、lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
2、我们也可以直接在 lambda 表达式中访问外层的局部变量
3、lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
4、在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
方法引用通过方法的名字来指向一个方法。
方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
方法引用使用一对冒号 :: 。
package com.runoob.main;
@FunctionalInterface
public interface Supplier {
T get();
}
class Car {
//Supplier是jdk1.8的接口,这里和lamda一起使用了
public static Car create(final Supplier 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,实例如下:
cars.forEach( Car::collide );
特定类的任意对象的方法引用:它的语法是Class::method实例如下:
cars.forEach( Car::repair );
特定对象的方法引用:它的语法是instance::method实例如下:
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
Lambda 表达式和方法引用(实际上也可认为是Lambda表达式)上。
Java8的新引入,包含函数式的设计,接口都有@FunctionalInterface的注解。就像这个注解的注释说明一样,它注解在接口层面,且注解的接口要有且仅有一个抽象方法。具体就是说,注解在Inteface上,且interface里只能有一个抽象方法,可以有default方法。
@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
}
那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):
GreetingService greetService1 = message -> System.out.println("Hello " + message);
函数式接口可以对现有的函数友好地支持 lambda。
JDK 1.8 之前已有的函数式接口:
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
JDK 1.8 新增加的函数接口:
java.util.function
java.util.function 它包含了很多类,用来支持 Java的 函数式编程,
Predicate
该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。
该接口用于测试对象是 true 或 false。
我们可以通过以下实例(Java8Tester.java)来了解函数式接口 Predicate
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class Java8Tester {
public static void main(String args[]){
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// Predicate predicate = n -> true
// n 是一个参数传递到 Predicate 接口的 test 方法
// n 如果存在则 test 方法返回 true
System.out.println("输出所有数据:");
// 传递参数 n
eval(list, n->true);
// Predicate predicate1 = n -> n%2 == 0
// n 是一个参数传递到 Predicate 接口的 test 方法
// 如果 n%2 为 0 test 方法返回 true
System.out.println("输出所有偶数:");
eval(list, n-> n%2 == 0 );
// Predicate predicate2 = n -> n > 3
// n 是一个参数传递到 Predicate 接口的 test 方法
// 如果 n 大于 3 test 方法返回 true
System.out.println("输出大于 3 的所有数字:");
eval(list, n-> n > 3 );
}
public static void eval(List list, Predicate predicate) {
for(Integer n: list) {
if(predicate.test(n)) {
System.out.println(n + " ");
}
}
}
}
Java 8 新增了接口的默认方法。
简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。
我们只需在方法名前面加个 default 关键字即可实现默认方法。
首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
}
一个接口有默认方法,考虑这样的情况,一个类实现了多个接口,且这些接口有相同的默认方法,以下实例说明了这种情况的解决方法:
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
}
public interface FourWheeler {
default void print(){
System.out.println("我是一辆四轮车!");
}
}
第一个解决方案是创建自己的默认方法,来覆盖重写接口的默认方法:
public class Car implements Vehicle, FourWheeler {
default void print(){
System.out.println("我是一辆四轮汽车!");
}
}
第二种解决方案可以使用 super 来调用指定接口的默认方法:
public class Car implements Vehicle, FourWheeler {
public void print(){
Vehicle.super.print();
}
}
Java 8 的另一个特性是接口可以声明(并且可以提供实现)静态方法。例如:
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
// 静态方法
static void blowHorn(){
System.out.println("按喇叭!!!");
}
}
Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
Stream(流)是一个来自数据源的元素队列并支持聚合操作
元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:
Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
在 Java 8 中, 集合接口有两个方法来生成流:
stream() − 为集合创建串行流。
parallelStream() − 为集合创建并行流。
List strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
Stream 提供了新的方法 'forEach' 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 获取对应的平方数
List squaresList = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
filter 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤出空字符串:
Liststrings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
int count = strings.stream().filter(string -> string.isEmpty()).count();
limit 方法用于获取指定数量的流。 以下代码片段使用 limit 方法打印出 10 条数据:
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法对输出的 10 个随机数进行排序:
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);
parallelStream 是流并行处理程序的代替方法。以下实例我们使用 parallelStream 来输出空字符串的数量:
List strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
int count = strings.parallelStream().filter(string -> string.isEmpty()).count();
Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors 可用于返回列表或字符串:
Liststrings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("筛选列表: " + filtered);
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("合并字符串: " + mergedString);
另外,一些产生统计结果的收集器也非常有用。它们主要用于int、double、long等基本类型上,它们可以用来产生类似如下的统计结果。
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : " + stats.getMax());
System.out.println("列表中最小的数 : " + stats.getMin());
System.out.println("所有数之和 : " + stats.getSum());
System.out.println("平均数 : " + stats.getAverage());
6、Optional 类
Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
Optional 类的引入很好的解决空指针异常。
public final class Optional extends Object
import java.util.Optional;
public class Java8Tester {
public static void main(String args[]){
Java8Tester java8Tester = new Java8Tester();
Integer value1 = null;
Integer value2 = new Integer(10);
// Optional.ofNullable - 允许传递为 null 参数
Optional a = Optional.ofNullable(value1);
// Optional.of - 如果传递的参数是 null,抛出异常 NullPointerException
Optional b = Optional.of(value2);
System.out.println(java8Tester.sum(a,b));
}
public Integer sum(Optional a, Optional b){
// Optional.isPresent - 判断值是否存在
System.out.println("第一个参数值存在: " + a.isPresent());
System.out.println("第二个参数值存在: " + b.isPresent());
// Optional.orElse - 如果值存在,返回它,否则返回默认值
Integer value1 = a.orElse(new Integer(0));
//Optional.get - 获取值,值需要存在
Integer value2 = b.get();
return value1 + value2;
}
}
Nashorn 一个 javascript 引擎。
从JDK 1.8开始,Nashorn取代Rhino(JDK 1.6, JDK1.7)成为Java的嵌入式JavaScript引擎。Nashorn完全支持ECMAScript 5.1规范以及一些扩展。它使用基于JSR 292的新语言特性,其中包含在JDK 7中引入的 invokedynamic,将JavaScript编译成Java字节码。
与先前的Rhino实现相比,这带来了2到10倍的性能提升。jjs是个基于Nashorn引擎的命令行工具。它接受一些JavaScript源代码为参数,并且执行这些源代码。
例如,我们创建一个具有如下内容的sample.js文件:
print('Hello World!');
打开控制台,输入以下命令:
$ jjs sample.js
以上程序输出结果为:
Hello World!
$ jjs
jjs> print("Hello, World!")
Hello, World!
jjs> quit()
>>
$ jjs -- a b c
jjs> print('字母: ' +arguments.join(", "))
字母: a, b, c
jjs>
import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
public class Java8Tester {
public static void main(String args[]){
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn");
String name = "Runoob";
Integer result = null;
try {
nashorn.eval("print('" + name + "')");
result = (Integer) nashorn.eval("10 + 2");
}catch(ScriptException e){
System.out.println("执行脚本错误: "+ e.getMessage());
}
System.out.println(result.toString());
}
}
以下实例演示了如何在 JavaScript 中引用 Java 类:
var BigDecimal = Java.type('java.math.BigDecimal');
function calculate(amount, percentage) {
var result = new BigDecimal(amount).multiply(
new BigDecimal(percentage)).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_EVEN);
return result.toPlainString();
}
var result = calculate(568000000000000000023,13.9);
print(result);
我们使用 jjs 命令执行以上脚本,输出结果如下:
$ jjs sample.js
78952000000000002017.94
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:
1、非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
2、设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在3、3、java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
4、时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:
Local(本地) − 简化了日期时间的处理,没有时区的问题。
Zoned(时区) − 通过制定的时区处理日期时间。
新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。
LocalDate/LocalTime 和 LocalDateTime 类可以在处理时区不是必须的情况。
// 获取当前的日期时间
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("当前时间: " + currentTime);
LocalDate date1 = currentTime.toLocalDate();
System.out.println("date1: " + date1);
Month month = currentTime.getMonth();
int day = currentTime.getDayOfMonth();
int seconds = currentTime.getSecond();
System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);
LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
System.out.println("date2: " + date2);
// 12 december 2014
LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
System.out.println("date3: " + date3);
// 22 小时 15 分钟
LocalTime date4 = LocalTime.of(22, 15);
System.out.println("date4: " + date4);
// 解析字符串
LocalTime date5 = LocalTime.parse("20:15:30");
System.out.println("date5: " + date5);
如果我们需要考虑到时区,就可以使用时区的日期时间API:
public void testZonedDateTime(){
// 获取当前时间日期
ZonedDateTime date1 = ZonedDateTime.parse("2015-12-03T10:15:30+05:30[Asia/Shanghai]");
System.out.println("date1: " + date1);
ZoneId id = ZoneId.of("Europe/Paris");
System.out.println("ZoneId: " + id);
ZoneId currentZone = ZoneId.systemDefault();
System.out.println("当期时区: " + currentZone);
}
在Java 8中,Base64编码已经成为Java类库的标准。
Java 8 内置了 Base64 编码的编码器和解码器。
Base64工具类提供了一套静态方法获取下面三种BASE64编解码器:
基本:输出被映射到一组字符A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持A-Za-z0-9+/。
URL:输出映射到一组字符A-Za-z0-9+_,输出是URL和文件。
MIME:输出隐射到MIME友好格式。输出每行不超过76字符,并且使用'\r'并跟随'\n'作为分割。编码输出最后没有行分割。
static class Base64.Decoder
该类实现一个解码器用于,使用 Base64 编码来解码字节数据。
static class Base64.Encoder
该类实现一个编码器,使用 Base64 编码来编码字节数据。
Java 9 最大的变化之一是引入了模块系统(Jigsaw 项目)。
模块化的功能有几个目的:
1、让Java的SE程序更加容易轻量级部署(引入更少的依赖class,编译更少的class)
2、改进组件间的依赖管理,引入比Jar粒度更大的Module
3、改进性能和安全性
4、编译多版本兼容
如果用更加简单解释,那就是"解决Classpath地狱问题,改进部署能力"。
模块就是代码和数据的封装体。模块的代码被组织成多个包(package),每个包中包含Java类和接口;模块的数据则包括资源文件和其他静态信息。
Java 9 模块的重要特征是在其工件(artifact)的根目录中包含了一个描述模块的 module-info.class 文 件。 文件的格式可以是传统的 JAR 文件或是 Java 9 新增的 JMOD 文件。这个文件由根目录中的源代码文件 module-info.java 编译而来。该模块声明文件可以描述模块的不同特征。
在 module-info.java 文件中,我们可以用新的关键词module来声明一个模块,如下所示。下面给出了一个模块com.mycompany.mymodule的最基本的模块声明。
module com.runoob.mymodule {
}
模块的是通过module-info.java进行定义,编译后打包后,就成为一个模块的实体;在模块的定义文件中,我们需要指定模块之间的依赖靠关系,可以exports给那些模块用,需要使用那些模块(requires) 。下面是一个例子:
module com.foo.bar {
requires org.baz.qux;
exportscom.foo.bar.alpha;
exportscom.foo.bar.beta;
}
META-INF/
META-INF/MANIFEST.MF
module-info.class
com/foo/bar/alpha/AlphaFactory.class
com/foo/bar/alpha/Alpha.class
...
对于Java代码,模块可以看做零个或多个包的集合。除了其名称,模块定义包括以下内容:
接下来我们创建一个 com.runoob.greetings 的模块。
第一步
创建文件夹 C:\>JAVA\src,然后在该目录下再创建与模块名相同的文件夹 com.runoob.greetings。
第二步
在 C:\>JAVA\src\com.runoob.greetings 目录下创建 module-info.java 文件,代码如下:
module com.runoob.greetings { }
module-info.java 用于创建模块。这一步我们创建了 com.runoob.greetings 模块。
第三步
在模块中添加源代码文件,在目录 C:\>JAVA\src\com.runoob.greetings\com\runoob\greetings 中创建文件 Java9Tester.java,代码如下:
package com.runoob.greetings;
public class Java9Tester {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
第四步
创建文件夹 C:\>JAVA\mods,然后在该目录下创建 com.runoob.greetings 文件夹,编译模块到这个目录下:
C:/>JAVA> javac -d mods/com.runoob.greetings
src/com.runoob.greetings/module-info.java
src/com.runoob.greetings/com/runoob/greetings/Java9Tester.java
第五步
执行模块,查看输出结果:
C:/>JAVA> java --module-path mods -m com.runoob.greetings/com.runoob.greetings.Java9Tester
Hello World!
module-path 指定了模块所在的路径。
-m 指定主要模块。
多版本兼容 JAR 功能能让你创建仅在特定版本的 Java 环境中运行库程序时选择使用的 class 版本。
通过 --release 参数指定编译版本。
具体的变化就是 META-INF 目录下 MANIFEST.MF 文件新增了一个属性:
Multi-Release: true
在以下实例中,我们使用多版本兼容 JAR 功能将 Tester.java 文件生成了两个版本的 jar 包, 一个是 jdk 7,另一个是 jdk 9,然后我们再不同环境下执行。
创建文件夹 c:/test/java7/com/runoob,并在该文件夹下创建 Test.java 文件,代码如下:
package com.runoob;
public class Tester {
public static void main(String[] args) {
System.out.println("Inside java 7");
}
}
创建文件夹 c:/test/java9/com/runoob,并在该文件夹下创建 Test.java 文件,代码如下:
package com.runoob;
public class Tester {
public static void main(String[] args) {
System.out.println("Inside java 9");
}
}
编译源代码:
C:\test > javac --release 9 java9/com/runoob/Tester.java
C:\JAVA > javac --release 7 java7/com/runoob/Tester.java
创建多版本兼容 jar 包
C:\JAVA > jar -c -f test.jar -C java7 . --release 9 -C java9.
Warning: entry META-INF/versions/9/com/runoob/Tester.java,
multiple resources with same name
使用 JDK 7 执行:
C:\JAVA > java -cp test.jar com.tutorialspoint.Tester
Inside Java 7
使用 JDK 9 执行:
C:\JAVA > java -cp test.jar com.tutorialspoint.Tester
Inside Java 9