Java面试系列总结 :JavaSE高级(下)

1. Java的类加载器的种类都有哪些?

  • 根类加载器(Bootstrap) --C++写的 ,看不到源码
  • 扩展类加载器(Extension) --加载位置 :jre\lib\ext中
  • 系统(应用)类加载器(System\App) --加载位置 :classpath中
  • 自定义加载器(必须继承ClassLoader)

2. 类什么时候被初始化?

  • 1)创建类的实例,也就是new一个对象
  • 2)访问某个类或接口的静态变量,或者对该静态变量赋值
  • 3)调用类的静态方法
  • 4)反射(Class.forName(“com.lyj.load”))
  • 5)初始化一个类的子类(会首先初始化子类的父类)
  • 6)JVM启动时标明的启动类,即文件名和类名相同的那个类

只有这6中情况才会导致类的类的初始化。

类的初始化步骤:

  • 1)如果这个类还没有被加载和链接,那先进行加载和链接
  • 2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
  • 3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。

3. Java类加载体系之ClassLoader双亲委托机制

java 是一种类型安全的语言,它有四类称为安全沙箱机制的安全机制来保证语言的安全性,这四类安全沙箱分别是:

  • 1) 类加载体系
  • 2) .class文件检验器
  • 3) 内置于Java虚拟机(及语言)的安全特性
  • 4) 安全管理器及Java API

主要讲解类的加载体系:
java程序中的 .java文件编译完会生成 .class文件,而 .class文件就是通过被称为类加载器的ClassLoader加载的,而ClassLoder在加载过程中会使用“双亲委派机制”来加载 .class文件,先上图:
在这里插入图片描述
BootStrapClassLoader : 启 动 类 加 载 器 , 该 ClassLoader 是 jvm 在 启 动 时 创 建 的 , 用 于 加载 $JAVA_HOME$/jre/lib下面的类库(或者通过参数-Xbootclasspath指定)。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。

ExtClassLoader:扩展类加载器,该ClassLoader是在sun.misc.Launcher里作为一个内部类ExtClassLoader定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader 会加载 $JAVA_HOME$/jre/lib/ext 下的类库(或者通过参数-Djava.ext.dirs指定)。

AppClassLoader:应用程序类加载器,该 ClassLoader 同样是在 sun.misc.Launcher 里作为一个内部类AppClassLoader定义的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader会加载java环境变量CLASSPATH 所指定的路径下的类库 , 而CLASSPATH 所指定的路径可以通过System.getProperty(“java.class.path”)获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的class目录)。

CustomClassLoader:自定义类加载器,该ClassLoader是指我们自定义的ClassLoader,比如tomcat的StandardClassLoader属于这一类;当然,大部分情况下使用AppClassLoader就足够了。

前面谈到了ClassLoader的几类加载器,而ClassLoader使用双亲委派机制来加载class文件的。ClassLoader的双亲委派机制是这样的(这里先忽略掉自定义类加载器CustomClassLoader):

  • 1)当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 2)当 ExtClassLoader 加载一个 class 时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 3)如果 BootStrapClassLoader 加载失败(例如在$JAVA_HOME$/jre/lib 里未查找到该 class),会使用ExtClassLoader来尝试加载;
  • 4)若 ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

下面贴下ClassLoader的loadClass(String name, boolean resolve)的源码:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 
    // 首先找缓存是否有 class 
    Class c = findLoadedClass(name); 
    if (c == null) { 
        //没有判断有没有父类 
        try { 
            if (parent != null) { 
                //有的话,用父类递归获取 class 
                c = parent.loadClass(name, false); 
            } else { 
                //没有父类。通过这个方法来加载 
                c = findBootstrapClassOrNull(name); 
            } 
        } catch (ClassNotFoundException e) { 
            // ClassNotFoundException thrown if class not found 
            // from the non-null parent class loader 
        } 
        if (c == null) { 
            // 如果还是没有找到,调用 findClass(name)去找这个类 
            c = findClass(name); 
        } 
    } 
    if (resolve) { 
        resolveClass(c); 
    } 
    return c; 
} 

代码很明朗:首先找缓存(findLoadedClass),没有的话就判断有没有parent,有的话就用parent来递归的loadClass,然而ExtClassLoader并没有设置parent,则会通过findBootstrapClassOrNull来加载class,而findBootstrapClassOrNull则会通过JNI方法”private native Class findBootstrapClass(String name)“来使用BootStrapClassLoader来加载class。

然后如果 parent 未找到class,则会调用 findClass 来加载 class,findClass 是一个 protected 的空方法,可以覆盖它以便自定义class加载过程。

另外,虽然 ClassLoader 加载类是使用 loadClass 方法,但是鼓励用 ClassLoader 的子类重写 findClass(String),而不是重写loadClass,这样就不会覆盖了类加载默认的双亲委派机制。

双亲委派托机制为什么安全

举个例子,ClassLoader加载的class文件来源很多,比如编译器编译生成的class、或者网络下载的字节码。而一些来源的 class 文件是不可靠的,比如我可以自定义一个 java.lang.Integer 类来覆盖 jdk 中默认的 Integer类。

例如下面这样:

package java.lang; 
public class Integer { 
    public Integer(int value) { 
        System.exit(0); 
    } 
}

初始化这个Integer的构造器是会退出JVM,破坏应用程序的正常进行,如果使用双亲委派机制的话该Integer类永远不会被调用,以为委托BootStrapClassLoader加载后会加载JDK中的Integer类而不会加载自定义的这个,可以看下下面这测试个用例:

public static void main(String... args) { 
    Integer i = new Integer(1); 
    System.err.println(i); 
}

执行时JVM并未在new Integer(1)时退出,说明未使用自定义的Integer,于是就保证了安全性。

4. 描述一下JVM加载class

JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。

由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对类进行初始化,包括:

如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;

如果类中存在初始化语句,就依次执行这些初始化语句。类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader 的子类)。

从 Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的 Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java 程序提供对 Bootstrap 的引用。

下面是关于几个类加载器的说明:

  • Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);
  • Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap;
  • System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量classpath 或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。

5. 获得一个类对象有哪些方式?

  • 类型.class,例如:String.class
  • 对象.getClass(),例如:”hello”.getClass()
  • Class.forName(),例如:Class.forName(“java.lang.String”)

6. 既然有GC机制,为什么还会有内存泄露的情况

理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因)。然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。

例如 hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。

下面例子中的代码也会导致内存泄露。

import java.util.Arrays; 
import java.util.EmptyStackException; 

public class MyStack<T> { 

    private T[] elements; 
    private int size = 0; 
    private static final int INIT_CAPACITY = 16; 
    
    public MyStack() { 
        elements = (T[]) new Object[INIT_CAPACITY]; 
    } 
    public void push(T elem) { 
        ensureCapacity(); 
        elements[size++] = elem; 
    } 
    public T pop() { 
        if(size == 0)throw new EmptyStackException(); 
        return elements[--size]; 
    } 
    private void ensureCapacity() { 
        if(elements.length == size) { 
            elements = Arrays.copyOf(elements, 2 * size + 1); 
        } 
    } 
}

上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的 pop 方法却存在内存泄露的问题,当我们用 pop 方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发 Disk Paging (物理内存与硬盘的虚拟内存交换数据),甚至造成 OutOfMemoryError。

7. Java中为什么会有GC机制呢?

Java中为什么会有GC机制呢?

  • 安全性考虑;-- for security.
  • 减少内存泄露;-- erase memory leak in some degree.
  • 减少程序员工作量。-- Programmers don’t worry about memory releasing.

8. 对于Java的GC哪些内存需要回收

内存运行时 JVM 会有一个运行时数据区来管理内存。它主要包括 5 大部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap).

而其中程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡。例如栈中每一个栈帧中分配多少内存基本上在类结构确定是哪个时就已知了,因此这 3 个区域的内存分配和回收都是确定的,无需考虑内存回收的问题。

但方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,我们只有在程序运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注的是这部分内存。

总而言之,GC 主要进行回收的内存是JVM中的方法区和堆;

9. Java的GC什么时候回收垃圾

在面试中经常会碰到这样一个问题(事实上笔者也碰到过):如何判断一个对象已经死去?

很容易想到的一个答案是:对一个对象添加引用计数器。每当有地方引用它时,计数器值加1;当引用失效时,计数器值减 1.而当计数器的值为 0 时这个对象就不会再被使用,判断为已死。是不是简单又直观。然而,很遗憾。这种做法是错误的!为什么是错的呢?事实上,用引用计数法确实在大部分情况下是一个不错的解决方案,而在实际的应用中也有不少案例,但它却无法解决对象之间的循环引用问题。比如对象 A中有一个字段指向了对象B,而对象B中
也有一个字段指向了对象A,而事实上他们俩都不再使用,但计数器的值永远都不可能为0,也就不会被回收,然后就发生了内存泄露。

所以,正确的做法应该是怎样呢?

在Java,C#等语言中,比较主流的判定一个对象已死的方法是:可达性分析(Reachability Analysis). 所有生成的对象都是一个称为"GC Roots"的根的子树。从 GC Roots 开始向下搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链可以到达时,就称这个对象是不可达的(不可引用的),也就是可以被GC 回收了。

无论是引用计数器还是可达性分析,判定对象是否存活都与引用有关!那么,如何定义对象的引用呢?

我们希望给出这样一类描述:当内存空间还够时,能够保存在内存中;如果进行了垃圾回收之后内存空间仍旧非常紧张,则可以抛弃这些对象。所以根据不同的需求,给出如下四种引用,根据引用类型的不同,GC回收时也会有不同的操作:

  1. 强引用(Strong Reference):Object obj = new Object();只要强引用还存在,GC永远不会回收掉被引用的对象。

  2. 软引用(Soft Reference):描述一些还有用但非必需的对象。在系统将会发生内存溢出之前,会把这些对象列入回收范围进行二次回收(即系统将会发生内存溢出了,才会对他们进行回收。)

  3. 弱引用(Weak Reference):程度比软引用还要弱一些。这些对象只能生存到下次GC之前。当GC 工作时,无论内存是否足够都会将其回收(即只要进行GC,就会对他们进行回收。)

  4. 虚引用(Phantom Reference):一个对象是否存在虚引用,完全不会对其生存时间构成影响。

关于方法区中需要回收的是一些废弃的常量和无用的类。

1).废弃的常量的回收。这里看引用计数就可以了。没有对象引用该常量就可以放心的回收了。

2).无用的类的回收。什么是无用的类呢?

  • A.该类所有的实例都已经被回收。也就是Java堆中不存在该类的任何实例;
  • B.加载该类的ClassLoader已经被回收;
  • C.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

总而言之:

对于堆中的对象,主要用可达性分析判断一个对象是否还存在引用,如果该对象没有任何引用就应该被回收。而根据我们实际对引用的不同需求,又分成了4中引用,每种引用的回收机制也是不同的。

对于方法区中的常量和类,当一个常量没有任何对象引用它,它就可以被回收了。而对于类,如果可以判定它为无用类,就可以被回收了。

10. 通过10个示例来初步认识Java8中的lambda表达式

我个人对Java 8发布非常激动,尤其是lambda表达式和流API。越来越多的了解它们,我能写出更干净的代码。虽然一开始并不是这样。第一次看到用lambda表达式写出来的Java代码时,我对这种神秘的语法感到非常失望,认为它们把Java搞得不可读,但我错了。花了一天时间做了一些lambda表达式和流API示例的练习后,我开心的看到了更清晰的Java代码。这有点像学习泛型,第一次见的时候我很讨厌它。我甚至继续使用老版Java 1.4来处理集合,直到有一天,朋友跟我介绍了使用泛型的好处(才意识到它的好处)。所以基本立场就是,不要畏惧 lambda 表达式以及方法引用的神秘语法,做几次练习,从集合类中提取、过滤数据之后,你就会喜欢上它。下面让我们开启学习Java 8 lambda表达式的学习之旅吧~

本小节中先不说lambda表达的含义和繁琐的概念。我们先从最简单的示例来介绍java8中的lambda表达式

例1、用 lambda表达式实现Runnable

// Java 8 之前: 
new Thread(new Runnable() {     
    @Override     
    public void run() {       
        System.out.println("Before Java8, too much code for too little to do");     
    } 
}).start(); 
 
//Java 8 方式: 
new Thread( () -> System.out.println("In Java8, Lambda expression rocks !!") ).start(); 

这个例子向我们展示了Java 8 lambda表达式的语法。你可以使用lambda写出如下代码:

(params) -> expression 
(params) -> statement 
(params) -> { statements } 

例如,如果你的方法不对参数进行修改、重写,只是在控制台打印点东西的话,那么可以这样写:

() -> System.out.println("Hello Lambda Expressions"); 

如果你的方法接收两个参数,那么可以写成如下这样:

(int even, int odd) -> even + odd 

顺便提一句,通常都会把lambda表达式内部变量的名字起得短一些。这样能使代码更简短,放在同一行。所以,在上述代码中,变量名选用a、b或者x、y会比even、odd要好。

例2、使用Java 8 lambda表达式进行事件处理

如果你用过Swing API编程,你就会记得怎样写事件监听代码。这又是一个旧版本简单匿名类的经典用例,但现在可以不这样了。你可以用lambda表达式写出更好的事件监听代码,如下所示:

// Java 8 之前: 
JButton show =  new JButton("Show"); show.addActionListener(new ActionListener() { 
    @Override     
    public void actionPerformed(ActionEvent e) {     
        System.out.println("Event handling without lambda expression is boring");     
    } 
}); 
 
// Java 8 方式: 
show.addActionListener((e) -> {     
    System.out.println("Light, Camera, Action !! Lambda expressions Rocks"); 
});

Java 开发者经常使用匿名类的另一个地方是为 Collections.sort() 定制 Comparator。在 Java 8 中,你可以用更可读的 lambda 表达式换掉丑陋的匿名类。我把这个留做练习,应该不难,可以按照我在使用 lambda 表达式实现 Runnable 和 ActionListener 的过程中的套路来做。

例3、使用Java 8 lambda表达式进行事件处理 使用lambda表达式对列表进行迭代

如果你使过几年 Java,你就知道针对集合类,最常见的操作就是进行迭代,并将业务逻辑应用于各个元素,例如处理订单、交易和事件的列表。由于Java是命令式语言,Java 8之前的所有循环代码都是顺序的,即可以对其元素进行并行化处理。如果你想做并行过滤,就需要自己写代码,这并不是那么容易。通过引入lambda表达式和默认方法,将做什么和怎么做的问题分开了,这意味着Java集合现在知道怎样做迭代,并可以在API层面对集合元素进行并行处理。下面的例子里,我将介绍如何在使用 lambda 或不使用 lambda 表达式的情况下迭代列表。你可以看到列表现在有了一个 forEach() 方法,它可以迭代所有对象,并将你的lambda代码应用在其中。

// Java 8 之前: 
List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API"); 
for (String feature : features) {     
    System.out.println(feature); 
} 
 
// Java 8 之后: 
List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API"); 
features.forEach(n -> System.out.println(n));   
// 使用 Java 8 的方法引用更方便,方法引用由::双冒号操作符标示, 
// 看起来像 C++的作用域解析运算符 
features.forEach(System.out::println); 

列表循环的最后一个例子展示了如何在Java 8中使用方法引用(method reference)。你可以看到C++里面的双冒号、范围解析操作符现在在Java 8中用来表示方法引用。

例4、使用lambda表达式和函数式接口Predicate

除了在语言层面支持函数式编程风格,Java 8也添加了一个包,叫做 java.util.function。它包含了很多类,用来支持 Java 的函数式编程。其中一个便是 Predicate,使用 java.util.function.Predicate 函数式接口以及 lambda 表达式,可以向API方法添加逻辑,用更少的代码支持更多的动态行为。下面是Java 8 Predicate 的例子,展示了过滤集合数据的多种常用方法。Predicate接口非常适用于做过滤。

public static void main(args[]){     
    List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");       
    System.out.println("Languages which starts with J :");     
    filter(languages, (str)->str.startsWith("J"));       
    System.out.println("Languages which ends with a ");     
    filter(languages, (str)->str.endsWith("a")); 
    System.out.println("Print all languages :");     
    filter(languages, (str)->true);       
    System.out.println("Print no language : ");     
    filter(languages, (str)->false);       
    System.out.println("Print language whose length greater than 4:");     
    filter(languages, (str)->str.length() > 4); 
}   

public static void filter(List names, Predicate condition) {     
    for(String name: names)  { 
        if(condition.test(name)) {             
            System.out.println(name + " ");         
        }     
    } 
} 
 
 
// filter 更好的办法--filter 方法改进 
public static void filter(List names, Predicate condition) {     
    names.stream().filter((name) -> (condition.test(name))).forEach((name) -> {         
        System.out.println(name + " ");     
    }); 
}  

可以看到,Stream API 的过滤方法也接受一个 Predicate,这意味着可以将我们定制的 filter() 方法替换成写在里面的内联代码,这就是lambda表达式的魔力。另外,Predicate接口也允许进行多重条件的测试,下个例子将要讲到。

例5、如何在lambda表达式中加入Predicate

上个例子说到,java.util.function.Predicate 允许将两个或更多的 Predicate 合成一个。它提供类似于逻辑操作符AND和OR的方法,名字叫做and()、or()和xor(),用于将传入 filter() 方法的条件合并起来。例如,要得到所有以J开始,长度为四个字母的语言,可以定义两个独立的 Predicate 示例分别表示每一个条件,然后用 Predicate.and() 方法将它们合并起来,如下所示:

// 甚至可以用 and()、or()和 xor()逻辑函数来合并 Predicate, 
// 例如要找到所有以 J 开始,长度为四个字母的名字,你可以合并两个 
Predicate 并传入 Predicate<String> startsWithJ = (n) -> n.startsWith("J"); 
Predicate<String> fourLetterLong = (n) -> n.length() == 4; 
names.stream().filter(startsWithJ.and(fourLetterLong)).forEach((n) -> System.out.print("nName, which starts with 'J' and four letter long is : " + n));

类似地,也可以使用 or() 和 xor() 方法。本例着重介绍了如下要点:可按需要将 Predicate 作为单独条件然后将其合并起来使用。简而言之,你可以以传统 Java 命令方式使用 Predicate 接口,也可以充分利用 lambda 表达式达到事半功倍的效果。

例6、Java 8中使用lambda表达式的Map和Reduce示例

本例介绍最广为人知的函数式编程概念map。它允许你将对象进行转换。例如在本例中,我们将 costBeforeTax 列表的每个元素转换成为税后的值。我们将 x -> x*x lambda表达式传到 map() 方法,后者将其应用到流中的每一个元素。然后用 forEach() 将列表元素打印出来。使用流API的收集器类,可以得到所有含税的开销。有 toList() 这样的方法将 map 或任何其他操作的结果合并起来。由于收集器在流上做终端操作,因此之后便不能重用流了。你甚至可以用流API的 reduce() 方法将所有数字合成一个,下一个例子将会讲到。

// 不使用 lambda 表达式为每个订单加上 12%的税 
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500); 
for (Integer cost : costBeforeTax) {     
    double price = cost + .12*cost;     
    System.out.println(price); 
}   

// 使用 lambda 表达式 
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500); 
costBeforeTax.stream().map((cost) -> cost + .12*cost).forEach(System.out::println); 

在上面例子中,可以看到map将集合类(例如列表)元素进行转换的。还有一个 reduce() 函数可以将所有值合并成一个。Map和Reduce操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。另外,reduce 并不是一个新的操作,你有可能已经在使用它。SQL 中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是 reduce 操作,因为它们接收多个值并返回一个值。流API定义的 reduceh() 函数可以接受lambda表达式,并对所有值进行合并。IntStream 这样的类有类似 average()、count()、sum() 的内建方法来做 reduce 操作,也有mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。在这个Java 8的Map Reduce示例里,我们首先对所有价格应用 12% 的VAT,然后用 reduce() 方法计算总和。

// 为每个订单加上 12%的税 
// 老方法: 
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500); 
double total = 0; 
for (Integer cost : costBeforeTax) {     
    double price = cost + .12*cost;     
    total = total + price; 
} 
System.out.println("Total : " + total);   
// 新方法: 
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500); 
double bill = costBeforeTax.stream().map((cost) -> cost + .12*cost).reduce((sum, cost) -> sum + cost).get(); 
System.out.println("Total : " + bill); 

例7、通过过滤创建一个String列表

过滤是 Java 开发者在大规模集合上的一个常用操作,而现在使用 lambda 表达式和流 API 过滤大规模数据集合是惊人的简单。流提供了一个 filter() 方法,接受一个 Predicate 对象,即可以传入一个lambda表达式作为过滤逻辑。下面的例子是用lambda表达式过滤Java集合,将帮助理解。

// 创建一个字符串列表,每个字符串长度大于 2 
List costBeforeTax = Arrays.asList("abc","bcd","defg","jk"); 
List<String> filtered = strList.stream().filter(x -> x.length()> 2).collect(Collectors.toList()); 
System.out.printf("Original List : %s, filtered list : %s %n", strList, filtered); 

输出:

Original List : [abc, , bcd, , defg, jk], filtered list : [abc, bcd, defg] 

另外,关于 filter() 方法有个常见误解。在现实生活中,做过滤的时候,通常会丢弃部分,但使用filter()方法则是获得一个新的列表,且其每个元素符合过滤原则。

例8、对列表的每个元素应用函数

我们通常需要对列表的每个元素使用某个函数,例如逐一乘以某个数、除以某个数或者做其它操作。这些操作都很适合用 map() 方法,可以将转换逻辑以lambda表达式的形式放在 map() 方法里,就可以对集合的各个元素进行转换了,如下所示。

// 将字符串换成大写并用逗号链接起来 
List<String> G7 = Arrays.asList("USA", "Japan", "France", "Germany", "Italy", "U.K.","Canada"); 
String G7Countries = G7.stream().map(x -> x.toUpperCase()).collect(Collectors.joining(", ")); 
System.out.println(G7Countries); 

输出:

USA, JAPAN, FRANCE, GERMANY, ITALY, U.K., CANADA 

例9、复制不同的值,创建一个子列表

本例展示了如何利用流的 distinct() 方法来对集合进行去重

// 用所有不同的数字创建一个正方形列表 
List<Integer> numbers = Arrays.asList(9, 10, 3, 4, 7, 3, 4); 
List<Integer> distinct = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList()); 
System.out.printf("Original List : %s,  Square Without duplicates : %s %n", numbers, distinct); 

输出:

Original List : [9, 10, 3, 4, 7, 3, 4],  Square Without duplicates : [81, 100, 9, 16, 49] 

例10、计算集合元素的最大值、最小值、总和以及平均值

IntStream、LongStream 和 DoubleStream 等流的类中,有个非常有用的方法叫做 summaryStatistics() 。可以返回 IntSummaryStatistics、LongSummaryStatistics 或者 DoubleSummaryStatistic s,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值。它也有 getSum() 和 getAverage() 方法来获得列表的所有元素的总和及平均值。

//获取数字的个数、最小值、最大值、总和以及平均值 
List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29); 
IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics(); 
System.out.println("Highest prime number in List : " + stats.getMax()); 
System.out.println("Lowest prime number in List : " + stats.getMin()); 
System.out.println("Sum of all prime numbers : " + stats.getSum()); 
System.out.println("Average of all prime numbers : " + stats.getAverage()); 

输出:

Highest prime number in List : 29 
Lowest prime number in List : 2 
Sum of all prime numbers : 129 
Average of all prime numbers : 12.9 

Java 8 的 10 个 lambda 表达式,这对于新手来说是个合适的任务量,你可能需要亲自运行示例程序以便掌握。试着修改要求创建自己的例子,达到快速学习的目的。

补充 从例子中我们可以可以看到,以前写的匿名内部类都用了 lambda 表达式代替了。那么,我们简单谈谈“lambda表达式&匿名内部类”

两者不用:

  1. 关键字this
    (1) 匿名内部类中的this代表匿名类
    (2) Lambda表达式中的this代表lambda表达式的类

  2. 编译方式不同
    (1) 匿名内部类中会编译成一个.class文件,文件命名方式为:主类+$+(1,2,3…)
    (2) Java 编译器将 lambda 表达式编译成类的私有方法。使用了 Java 7 的 invokedynamic 字节码指令来动态绑定这个方法

11. Java8中的lambda表达式要点

通过面10个小示例中学习,我们下面说下lambda表达式的6个要点

要点1:lambda表达式的使用位置

预定义使用了 @Functional 注释的函数式接口,自带一个抽象函数的方法,或者SAM(Single Abstract Method 单个抽象方法)类型。这些称为lambda表达式的目标类型,可以用作返回类型,或lambda目标代码的参数。例如,若一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier,那么可以向其传lambda表达式。

要点2:lambda表达式和方法引用

lambda 表达式内可以使用方法引用,仅当该方法不修改 lambda 表达式提供的参数。本例中的 lambda 表达式可以换为方法引用,因为这仅是一个参数相同的简单方法调用。

list.forEach(n -> System.out.println(n));  
list.forEach(System.out::println);  // 使用方法引用 

然而,若对参数有任何修改,则不能使用方法引用,而需键入完整地lambda表达式,如下所示:

list.forEach((String s) -> System.out.println("*" + s + "*")); 

事实上,可以省略这里的lambda参数的类型声明,编译器可以从列表的类属性推测出来。

要点3:lambda表达式内部引用资源

lambda内部可以使用静态、非静态和局部变量,这称为lambda内的变量捕获。

要点4:lambda表达式也成闭包

Lambda表达式在Java中又称为闭包或匿名函数,所以如果有同事把它叫闭包的时候,不用惊讶。

要点5:lambda表达式的编译方式

Lambda方法在编译器内部被翻译成私有方法,并派发 invokedynamic 字节码指令来进行调用。可以使用JDK中的 javap 工具来反编译class文件。使用 javap -p 或 javap -c -v 命令来看一看lambda表达式生成的字节码。大致应该长这样:

private static java.lang.Object lambda$0(java.lang.String); 

要点6:lambda表达式的限制

lambda 表达式有个限制,那就是只能引用 final 或 final 局部变量,这就是说不能在 lambda 内部修改定义在域外的变量。

List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7}); 
int factor = 2; 
primes.forEach(element -> { factor++; }); 

Error:

Compile time error : "local variables referenced from a lambda expression must be final or effectively final" 

另外,只是访问它而不作修改是可以的,如下所示:

List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7}); 
int factor = 2; 
primes.forEach(element -> { System.out.println(factor*element); }); 

输出:

4 
6 
10 
14 

因此,它看起来更像不可变闭包,类似于Python。

12. Java8中的Optional类的解析

身为一名Java程序员,大家可能都有这样的经历:调用一个方法得到了返回值却不能直接将返回值作为参数去调用别的方法。我们首先要判断这个返回值是否为 null,只有在非空的前提下才能将其作为其他方法的参数。这正是一些类似Guava的外部API试图解决的问题。一些JVM编程语言比如Scala、Ceylon等已经将对在核心API中解决了这个问题。在我的前一篇文章中,介绍了Scala是如何解决了这个问题。

新版本的Java,比如Java 8引入了一个新的Optional类。Optional类的Javadoc描述如下:

这是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回 true ,调用 get() 方法会返回该对象。

下面会逐个探讨Optional类包含的方法,并通过一两个示例展示如何使用。

方法1:Optional.of()

作用:为非null的值创建一个Optional。

说明:of 方法通过工厂方法创建Optional类。需要注意的是,创建对象时传入的参数不能为null。如果传入参数为null,则抛出NullPointerException 。

//调用工厂方法创建 Optional 实例 
Optional<String> name = Optional.of("Sanaulla"); 
//传入参数为 null,抛出 NullPointerException. 
Optional<String> someNull = Optional.of(null); 

方法2:Optional.ofNullable()

作用:为指定的值创建一个Optional,如果指定的值为null,则返回一个空的Optional。

说明:ofNullable与of 方法相似,唯一的区别是可以接受参数为null的情况。

//下面创建了一个不包含任何值的 Optional 实例 
//例如,值为'null' 
Optional empty = Optional.ofNullable(null); 

方法3:Optional.isPresent()

作用:判断预期值是否存在

说明:如果值存在返回true,否则返回false。

//isPresent 方法用来检查 Optional 实例中是否包含值 
Optional<String> name = Optional.of("Sanaulla"); 
if (name.isPresent()) { 
    //在 Optional 实例内调用 get()返回已存在的值   
    System.out.println(name.get());
    //输出 Sanaulla 
}

方法4:Optional.get()

作用:如果Optional有值则将其返回,否则抛出NoSuchElementException。

说明:上面的示例中,get方法用来得到Optional实例中的值。下面我们看一个抛出NoSuchElementException的例子

//执行下面的代码会输出:No value present  
try {        
    Optional empty = Optional.ofNullable(null); 
    //在空的 Optional 实例上调用 get(),抛出 NoSuchElementException   
    System.out.println(empty.get()); 
} catch (NoSuchElementException ex) { 
    System.out.println(ex.getMessage()); 
}

方法5:Optional.ifPresent()

作用:如果Optional实例有值则为其调用consumer,否则不做处理

说明:要理解 ifPresent 方法,首先需要了解 Consumer 类。简答地说,Consumer 类包含一个抽象方法。该抽象方法对传入的值进行处理,但没有返回值。Java8支持不用接口直接通过lambda表达式传入参数,如果Optional实例有值,调用ifPresent()可以接受接口段或lambda表达式

//ifPresent 方法接受 lambda 表达式作为参数。 
//lambda 表达式对 Optional 的值调用 consumer 进行处理。 
Optional<String> name = Optional.of("Sanaulla"); name.ifPresent((value) -> {   
    System.out.println("The length of the value is: " + value.length()); 
}); 

方法7:Optional.orElse()

作用:如果有值则将其返回,否则返回指定的其它值。

说明:如果Optional实例有值则将其返回,否则返回orElse方法传入的参数。示例如下:

Optional<String> name = Optional.of("Sanaulla"); 
Optional<String> someNull = Optional.of(null); 
//如果值不为 null,orElse 方法返回 Optional 实例的值。 
//如果为 null,返回传入的消息。 
//输出:There is no value present! 
System.out.println(empty.orElse("There is no value present!")); 
//输出:Sanaulla 
System.out.println(name.orElse("There is some value!")); 

方法8:Optional.orElseGet()

作用:如果有值则将其返回,否则返回指定的其它值。

说明:orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的字符串作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值

Optional<String> name = Optional.of("Sanaulla"); 
Optional<String> someNull = Optional.of(null); 
 
//orElseGet 与 orElse 方法类似,区别在于 orElse 传入的是默认值, 
//orElseGet 可以接受一个 lambda 表达式生成默认值。 
//输出:Default Value 
System.out.println(empty.orElseGet(() -> "Default Value")); 
//输出:Sanaulla 
System.out.println(name.orElseGet(() -> "Default Value")); 

方法9:Optional.orElseThrow()

作用:如果有值则将其返回,否则抛出supplier接口创建的异常。

说明:在orElseGet方法中,我们传入一个Supplier接口。然而,在orElseThrow中我们可以传入一个lambda表达式或方法,如果值不存在来抛出异常

try { 
    Optional<String> empty= Optional.of(null); 
    //orElseThrow 与 orElse 方法类似。与返回默认值不同,   
    //orElseThrow 会抛出 lambda 表达式或方法生成的异常    
    empty.orElseThrow(ValueAbsentException::new); 
} catch (Throwable ex) { 
    //输出: No value present in the Optional instance   
    System.out.println(ex.getMessage()); 
} 

ValueAbsentException 定义如下:

class ValueAbsentException extends Throwable { 
    
    public ValueAbsentException() {     
        super();   
    }     
    
    public ValueAbsentException(String msg) {     
        super(msg);   
    }     
    
    @Override   
    public String getMessage() { 
        return "No value present in the Optional instance";   
    } 
} 

方法10:Optional.map()

作用:如果有值,则对其执行调用mapping函数得到返回值。如果返回值不为null,则创建包含mapping返回值的Optional作为map方法返回值,否则返回空Optional。

说明:map 方法用来对 Optional 实例的值执行一系列操作。通过一组实现了 Function 接口的 lambda 表达式传入操作。

Optional<String> name = Optional.of("Sanaulla"); 
//map 方法执行传入的 lambda 表达式参数对 Optional 实例的值进行修改。 
//为 lambda 表达式的返回值创建新的 Optional 实例作为 map 方法的返回值。 
Optional<String> upperName = name.map((value) -> value.toUpperCase()); 
System.out.println(upperName.orElse("No value found")); 

方法11:Optional.flatMap()

作用:如果有值,为其执行mapping函数返回Optional类型返回值,否则返回空Optional。flatMap与map(Funtion)方法类似,区别在于flatMap中的mapper返回值必须是Optional。调用结束时,flatMap不会对结果用Optional封装。

说明:flatMap方法与map方法类似,区别在于mapping函数的返回值不同。map方法的mapping函数返回值可以是任何类型T,而flatMap方法的mapping函数必须是Optional。

Optional<String> name = Optional.of("Sanaulla"); 
//flatMap 与 map(Function)非常类似,区别在于传入方法的 lambda 表达式的返回类型。 
//map 方法中的 lambda 表达式返回值可以是任意类型,在 map 函数返回之前会包装为 Optional。  
//但 flatMap 方法中的 lambda 表达式返回值必须是 Optionl 实例。  
upperName = name.flatMap((value) -> Optional.of(value.toUpperCase())); 
System.out.println(upperName.orElse("No value found"));//输出 SANAULLA 

方法12:Optional.filter()

作用:如果有值并且满足断言条件返回包含该值的Optional,否则返回空Optional。

说明:filter 个方法通过传入限定条件对Optional实例的值进行过滤。这里可以传入一个 lambda 表达式。对于filter函数我们应该传入实现了Predicate接口的lambda表达式。

Optional<String> name = Optional.of("Sanaulla"); 
//filter 方法检查给定的 Option 值是否满足某些条件。 
//如果满足则返回同一个 Option 实例,否则返回空 Optional。 
Optional<String> longName = name.filter((value) -> value.length() > 6); 
System.out.println(longName.orElse("The name is less than 6 characters"));//输出 Sanaulla   

//另一个例子是 Optional 值不满足 filter 指定的条件。 
Optional<String> anotherName = Optional.of("Sana"); 
Optional<String> shortName = anotherName.filter((value) -> value.length() > 6); 
//输出:name 长度不足 6 字符 
System.out.println(shortName.orElse("The name is less than 6 characters")); 

总结:Optional方法

以上,我们介绍了Optional类的各个方法。下面通过一个完整的示例对用法集中展示。

public class OptionalDemo { 
    public static void main(String[] args) { 
        //创建 Optional 实例,也可以通过方法返回值得到。     
        Optional<String> name = Optional.of("Sanaulla");   
        //创建没有值的 Optional 实例,例如值为'null'     
        Optional empty = Optional.ofNullable(null); 
        //isPresent 方法用来检查 Optional 实例是否有值。     
        if (name.isPresent()) { 
            //调用 get()返回 Optional 值。       
            System.out.println(name.get());     
        }       
        
        try { 
            //在 Optional 实例上调用 get()抛出 NoSuchElementException。       
            System.out.println(empty.get());     
        } catch (NoSuchElementException ex) {       
            System.out.println(ex.getMessage());     
        } 
  
        //ifPresent 方法接受 lambda 表达式参数。     
        //如果 Optional 值不为空,lambda 表达式会处理并在其上执行操作。     
        name.ifPresent((value) -> {       
            System.out.println("The length of the value is: " + value.length());     
        });   
        
        //如果有值 orElse 方法会返回 Optional 实例,否则返回传入的错误信息。     
        System.out.println(empty.orElse("There is no value present!"));     
        System.out.println(name.orElse("There is some value!"));   
    
        //orElseGet 与 orElse 类似,区别在于传入的默认值。     
        //orElseGet 接受 lambda 表达式生成默认值。     
        System.out.println(empty.orElseGet(() -> "Default Value"));     
        System.out.println(name.orElseGet(() -> "Default Value"));       
        
        try { 
            //orElseThrow 与 orElse 方法类似,区别在于返回值。       
            //orElseThrow 抛出由传入的 lambda 表达式/方法生成异常。       
            empty.orElseThrow(ValueAbsentException::new);     
        } catch (Throwable ex) {       
            System.out.println(ex.getMessage()); 
        }   
        
        //map 方法通过传入的 lambda 表达式修改 Optonal 实例默认值。      
        //lambda 表达式返回值会包装为 Optional 实例。     
        Optional<String> upperName = name.map((value) -> value.toUpperCase());     
        System.out.println(upperName.orElse("No value found"));   
        
        //flatMap 与 map(Funtion)非常相似,区别在于 lambda 表达式的返回值。 
        //map 方法的 lambda 表达式返回值可以是任何类型,但是返回值会包装成 Optional 实例。     
        //但是 flatMap 方法的 lambda 返回值总是 Optional 类型。     
        upperName = name.flatMap((value) -> Optional.of(value.toUpperCase()));     
        System.out.println(upperName.orElse("No value found"));   
        
        //filter 方法检查 Optiona 值是否满足给定条件。     
        //如果满足返回 Optional 实例值,否则返回空 Optional。     
        Optional<String> longName = name.filter((value) -> value.length() > 6);     
        System.out.println(longName.orElse("The name is less than 6 characters"));   
        
        //另一个示例,Optional 值不满足给定条件。     
        Optional<String> anotherName = Optional.of("Sana"); 
        Optional<String> shortName = anotherName.filter((value) -> value.length() > 6);     
        System.out.println(shortName.orElse("The name is less than 6 characters"));     
    }   
}

13. 在开发中遇到过内存溢出么?原因有哪些?解决方法有哪些?

引起内存溢出的原因有很多种,常见的有以下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小;

内存溢出的解决方案:

  • 第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
  • 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
  • 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
    重点排查以下几点:
    1. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
    2. 检查代码中是否有死循环或递归调用。
    3. 检查是否有大循环重复产生新对象实体。
    4. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中 数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
    5. 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
  • 第四步,使用内存查看工具动态查看内存使用情况。

欢迎关注作者的公众号《Java编程生活》,每日记载Java程序猿工作中遇到的问题
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_26648623/article/details/84023296