学习 java 面试题(三)

  • IOC的优点是什么

要了解控制反转( Inversion of Control ), 我觉得有必要先了解软件设计的一个重要思想:依赖倒置原则(Dependency Inversion Principle )。

什么是依赖倒置原则?假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。

这样的设计看起来没问题,但是可维护性却很低。假设设计完工之后,上司却突然说根据市场需求的变动,要我们把车子的轮子设计都改大一码。这下我们就蛋疼了:因为我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改;同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改——整个设计几乎都得改!

我们现在换一种思路。我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身, 车身依赖汽车。

这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘,车身,汽车的设计了。

这就是依赖倒置原则——把原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的“牵一发动全身”的情况。

控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。其实这些概念初次接触都会感到云里雾里的。说穿了,这几种概念的关系大概如下:

为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码。我们先定义四个Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:

这样,就相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要这样改:

由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动:

由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。

所以我们需要进行控制反转(IoC),及上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:

这里我们再把轮胎尺寸变成动态的,同样为了让整个系统顺利运行,我们需要做如下修改:

看到没?这里我只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。不仅如此,在实际的工程中,这种设计模式还有利于不同组的协同合作和单元测试:比如开发这四个类的分别是四个不同的组,那么只要定义好了接口,四个不同的组可以同时进行开发而不相互受限制;而对于单元测试,如果我们要写Car类的单元测试,就只需要Mock一下Framework类传入Car就行了,而不用把Framework, Bottom, Tire全部new一遍再来构造Car。

这里我们是采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递和接口传递。这里就不多讲了,核心思路都是一样的,都是为了实现控制反转。
看到这里你应该能理解什么控制反转和依赖注入了。那什么是控制反转容器(IoC Container)呢?其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。

显然你也应该观察到了,因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。

IoC Container的第二个好处是:我们在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车instance时候,是从底层往上层new的:

这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入。

而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历):

这里IoC Container可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂:

我们就像是工厂的客户。我们只需要向工厂请求一个Car实例,然后它就给我们按照Config创建了一个Car实例。我们完全不用管这个Car实例是怎么一步一步被创建出来。

实际项目中,有的Service Class可能是十年前写的,有几百个类作为它的底层。假设我们新写的一个API需要实例化这个Service,我们总不可能回头去搞清楚这几百个类的构造函数吧?IoC Container的这个特性就很完美的解决了这类问题——因为这个架构要求你在写class的时候需要写相应的Config文件,所以你要初始化很久以前的Service类的时候,前人都已经写好了Config文件,你直接在需要用的地方注入这个Service就可以了。这大大增加了项目的可维护性且降低了开发难度。

这里只是很粗略的讲了一下我自己对IoC和DI的理解。主要的目的是在于最大限度避免晦涩难懂的专业词汇,用尽量简洁,通俗,直观的例子来解释这些概念。如果让大家能有一个类似“哦!原来就是这么个玩意嘛!”的印象,我觉得就OK了。想要深入了解的话,可以上网查阅一些更权威的资料。这里推荐一下 Dependency injection 和 Inversion of Control Containers and the Dependency Injection pattern 这两篇文章,讲的很好很详细。

https://www.zhihu.com/question/23277575/answer/169698662

  • IO 和 NIO的区别,NIO优点

IO是计算机中Input和Output简称,即输入和输出。 无论是系统、还是语言的设计中IO的设计都是异常复杂的。Java语言在IO设计方面是比较成功的,不仅是面向对象,而且利用装饰器设计模式(后面会写针对设计模式的文章)减少了大量的类,提供了较好的扩展性。
那Java IO怎么写入/读取数据?

Java IO类库可以分为输入流和输出流,输入流来读数据,输出流来写数据。

  • 输出流

实例代码一:

//输出流
public static void outputStream() throws IOException{
    //创建一个File实例
    File file = new File("/home/wenhaibo/IOTest.txt");
    //FileOutputStream为文件输出流
    FileOutputStream out = new FileOutputStream(file);
    //将内容转换为字节码输出
    out.write("This is IOTest".getBytes());
    //强制输出内存中所有内容
    out.flush();
    //关闭输出流
    out.close();
}
  • 输入流

实例代码二:

//输入流
public static void inputStream() throws IOException{
    //创建一个File实例
    File file = new File("/home/wenhaibo/IOTest.txt");
    //FileInputStream为文件输入流
    FileInputStream in = new FileInputStream(file);
    byte[] b = new byte[1024];
    //将 byte.length 个字节的数据读入一个 byte 数组中
    int len =in.read(b);
    //将字节码转为字符串打印输出
    System.out.println(new String(b, 0, len));
    //关闭输入流
    in.close();
}
  • 字节流和字符流

上面的实例中用的字节流方式写入/读取数据,Java IO不仅提供字节流方式还通过了字符流处理方式,处理字符流用Reader、Writer两个专门操作字符流的类。

  • Writer

实例代码三:

//字符流
public static void outputStreamWriter() throws IOException{
    //创建一个File实例
    File file = new File("/home/wenhaibo/IOTest.txt");
    //FileWriter为文件输出流
    Writer out = new FileWriter(file);
    //直接输出字符
    out.write("This is IOTest");
    //强制输出内存中所有内容
    out.flush();
    //关闭输出流
    out.close();
}
  • Reader

实例代码四:

public static void inputStreamReader() throws IOException{
    //创建一个File实例
    File file = new File("/home/wenhaibo/IOTest.txt");
    //Reader为文件输入流
    Reader in=new FileReader(file);
    char[] c=new char[1024];
    //将 byte.length 个字节的数据读入一个 byte 数组中
    int len =in.read(c);
    //将字节码转为字符串打印输出
    System.out.println(new String(c, 0, len));
    //关闭输入流
    in.close();

}
  • 缓冲流

Java缓冲流是在输入流和输出流之上进行了一次包装(装饰器设计模式),目的是解决频繁写入/读取数据时效率差的问题。缓冲流先将数据缓存起来,然后一起写入或读取出来。

字节缓冲流类:BufferedInputStream 和 BufferedOutputStream

字符缓冲流类:BufferedReader 和 BufferedWriter

flush()方法

当向文件写入数据时是先将输出流写入到缓冲区,当缓冲区写满后才将缓冲区的内容输出到文件中。但是当主机完成输出流的输出后,有可能缓冲区这个时候还没有被填满,这样的话,就会一直等待主机发送内容,这时候,就可以使用flush()方法将缓冲区的内容强制输出到文件中,清空缓冲区。

所以,一般在关闭输出流之前,要先调用flush()方法强制缓冲区中的内容输出,并清空缓冲区。

Java NIO的出现

因为传统的IO是阻塞而且低效的,JDK 1.4 提供了NIO(New IO)API。

那传统的IO和NIO有哪些不同?

面向流与面向缓冲

传统IO处理数据就像“小鸡啄米”,而NIO则是“狼吞虎咽”。

NIO中引入了缓冲区的概念,缓冲区作为传输数据的基本单位块,所有对数据的操作都是基于将数据移进/移出缓冲区而来;读数据的时候从缓冲区中取,写的时候将数据填入缓冲区。尽管传统JavaIO中也有相应的缓冲区过滤器流(BufferedInputStream等),但是移进/移出的操作是由程序员来包装的,它本质是对数据结构化和积累达到处理时的方便,并不是一种提高I/O效率的措施。NIO的缓冲区则不然,对缓冲区的移进/移出操作是由底层操作系统来实现的。

除了效率上的差别外,缓冲区在数据分析和处理上也带来的很大的便利和灵活性。

阻塞与非阻塞IO

传统JavaIO是基于阻塞I/O模型。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

  • Java NIO 核心API

Java NIO 中Channel,Buffer 和 Selector 构成了核心的API。

Channel

Channel(通道)和IO中的流是差不多一个等级的。只不过流是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。

NIO中的Channel的主要实现有:

FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel

分别可以对应文件IO、UDP和TCP(Server和Client)。
Buffer

NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。

选择器(Selectors)

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

实例代码五:

   //使用NOI输出
    public static void inputNIOChannel() throws IOException {
        //创建一个File实例
        File file = new File("/home/wenhaibo/IOTest.txt");
        //FileInputStream为文件输入流
        FileInputStream in = new FileInputStream(file);
        //缓冲器向通道输入数据
        FileChannel fileChannel = in.getChannel();
        //创建一个容量为1024字节的ByteBuffer
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //写入数据到Buffer
        int bytesRead = fileChannel.read(buf);
        while(bytesRead != -1)
        {
            //回绕缓冲区(输出通道会从数据的开头而不是末尾开始)
            buf.flip();
            while(buf.hasRemaining())
            {
                System.out.print((char)buf.get());
            }
            /**
             * 压缩此缓冲区,compact方法会执行两个动作
             * 1.清除之前写好的字符
             * 2.通过标记位置为0
             * 这就为什么要结合filp()使用
             */
            buf.compact();
            //写入数据到Buffer
            bytesRead = fileChannel.read(buf);
        }
    }

实际应用中在不考虑并发和读取/写入数据使用频率比较高的情况下Java IO已经可以胜任,但在使用到网络IO中Java IO已经不能满足实际需求,Java NIO 无疑是更好的选择。

https://zhuanlan.zhihu.com/p/37040086

  • Java 8 / Java 7 为我们提供了什么新功能
Java 7 的7个新特性
  1. 对集合类的语言支持;

  2. 自动资源管理;

  3. 改进的通用实例创建类型推断;

  4. 数字字面量下划线支持;

  5. switch中使用string;

  6. 二进制字面量;

  7. 简化可变参数方法调用。

JAVA8 新特性
  1. Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中。

  2. 方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

  3. 默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。

  4. 新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。

  5. Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。

  6. Date Time API − 加强对日期与时间的处理。

  7. Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。

  8. Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。

Java 7 的7个新特性
1.对集合类的语言支持

Java将包含对创建集合类的第一类语言支持。这意味着集合类的创建可以像Ruby和Perl那样了。

原本需要怎样:

复制代码

List<String> list = new ArrayList<String>();
         list.add("item");
         String item = list.get(0);
   
         Set<String> set = new HashSet<String>();
         set.add("item");
         Map<String, Integer> map = new HashMap<String, Integer>();
         map.put("key", 1);
         int value = map.get("key");

复制代码
现在只需这样:(这些集合是不可变的…)

复制代码

List<String> list = ["item"];
         String item = list[0];
         
         Set<String> set = {"item"};
         
         Map<String, Integer> map = {"key" : 1};
         int value = map["key"];

复制代码
2.自动资源管理

Java中某些资源是需要手动关闭的,如InputStream,Writes,Sockets,Sql classes等。这个新的语言特性允许try语句本身申请更多的资源,这些资源作用于try代码块,并自动关闭。

以前的写法:

BufferedReader br = new BufferedReader(new FileReader(path));
         try {
         return br.readLine();
               } finally {
                   br.close();
         }

现在可以:(有点像C#)

 try (BufferedReader br = new BufferedReader(new FileReader(path)) {
              return br.readLine();
          } 

3.改进的通用实例创建类型推断;

类型推断是一个特殊的烦恼,如下面的代码:

Map<String, List<String>> anagrams = new HashMap<String, List<String>>(); 

通过类型推断后变成:

Map<String, List<String>> anagrams = new HashMap<>();

注:这个<>被叫做diamond(钻石)运算符,Java 7后这个运算符从引用的声明中推断类型。

4.数字字面量下划线支持

很长的数字可读性不好,在Java 7中可以使用下划线分隔长int以及long了。如:

int one_million = 1_000_000;

这样子还真看不惯。。。不过的确是可读性好了。

5.switch中使用string

这个问题是我在Java中不喜欢用switch的原因之一,以前在switch中只能使用number或enum。现在可以使用string了,哈哈,不错,赞个!

复制代码

String s = ...
         switch(s) {
         case "quux":
              processQuux(s);
         // fall-through
          case "foo":
         case "bar":
              processFooOrBar(s);
         break;
         case "baz":
         processBaz(s);
              // fall-through
          default:
              processDefault(s);
         break;
         }

复制代码
6.二进制字面量

由于继承C语言,Java代码在传统上迫使程序员只能使用十进制,八进制或十六进制来表示数(numbers)。

由于很少的域是以bit导向的,这种限制可能导致错误。你现在可以使用0b前缀创建二进制字面量

int binary = 0b1001_1001; 

现在,可以使用二进制字面量这种表示方式,并且使用非常简短的代码,可将二进制字符转换为数据类型,如在byte或short。

 byte aByte = (byte)0b001;    
  short aShort = (short)0b010;

7.简化可变参数方法调用。

当程序员试图使用一个不可具体化的可变参数并调用一个varargs (可变)方法时,编辑器会生成一个“非安全操作”的警告。
JDK 7将警告从call转移到了方法声明(methord declaration)的过程中。这样API设计者就可以使用vararg,因为警告的数量大大减少了。

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

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

  1. 语言
  2. 编译器
  3. 工具
  4. 运行时(JVM)
  5. Java语言的新特性

Java 8是Java的一个重大版本,有人认为,虽然这些新特性领Java开发人员十分期待,但同时也需要花不少精力去学习。在这一小节中,我们将介绍Java 8的大部分新特性。

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

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

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );

在上面这个代码中的参数e的类型是由编译器推理得出的,你也可以显式指定该参数的类型,例如:

Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );

如果Lambda表达式需要更复杂的语句块,则可以使用花括号将该语句块括起来,类似于Java中的函数体,例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> {
    System.out.print( e );
    System.out.print( e );
} );

Lambda表达式可以引用类成员和局部变量(会将这些变量隐式得转换成final的),例如下列两个代码块的效果完全相同:

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

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

Lambda表达式有返回值,返回值的类型也由编译器推理得出。如果Lambda表达式中的语句块只有一行,则可以不用使用return语句,下列两个代码片段效果相同:

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

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
    int result = e1.compareTo( e2 );
    return result;
} );

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

@FunctionalInterface
public interface Functional {
    void method();
}

不过有一点需要注意,默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的。

复制代码

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();
 
    default void defaultMethod() {            
    }        
}

复制代码
Lambda表达式作为Java 8的最大卖点,它有潜力吸引更多的开发者加入到JVM平台,并在纯Java编程中使用函数式编程的概念。如果你需要了解更多Lambda表达式的细节,可以参考官方文档。

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

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

复制代码

private interface Defaulable {
    // Interfaces now allow default methods, the implementer may or 
    // may not implement (override) them.
    default String notRequired() { 
        return "Default implementation"; 
    }        
}
 
private static class DefaultableImpl implements Defaulable {
}
 
private static class OverridableImpl implements Defaulable {
    @Override
    public String notRequired() {
        return "Overridden implementation";
    }
}

复制代码
Defaulable接口使用关键字default定义了一个默认方法notRequired()。DefaultableImpl类实现了这个接口,同时默认继承了这个接口中的默认方法;OverridableImpl类也实现了这个接口,但覆写了该接口的默认方法,并提供了一个不同的实现。

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

private interface DefaulableFactory {
    // Interfaces now allow static methods
    static Defaulable create( Supplier< Defaulable > supplier ) {
        return supplier.get();
    }
}

下面的代码片段整合了默认方法和静态方法的使用场景:

复制代码

public static void main( String[] args ) {
    Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
    System.out.println( defaulable.notRequired() );
 
    defaulable = DefaulableFactory.create( OverridableImpl::new );
    System.out.println( defaulable.notRequired() );
}

复制代码
这段代码的输出结果如下:

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

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

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

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

复制代码
public static class Car {
public static Car create( final Supplier< Car > supplier ) {
return supplier.get();
}

public static void collide( final Car car ) {
    System.out.println( "Collided " + car.toString() );
}

public void follow( final Car another ) {
    System.out.println( "Following the " + another.toString() );
}

public void repair() {   
    System.out.println( "Repaired " + this.toString() );
}

}
复制代码
第一种方法引用的类型是构造器引用,语法是Class::new,或者更一般的形式:Class::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 );

运行上述例子,可以在控制台看到如下输出(Car实例可能不同):

Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d

如果想了解和学习更详细的内容,可以参考官方文档

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

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

复制代码

package com.javacodegeeks.java8.repeatable.annotations;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
public class RepeatingAnnotations {
    @Target( ElementType.TYPE )
    @Retention( RetentionPolicy.RUNTIME )
    public @interface Filters {
        Filter[] value();
    }
 
    @Target( ElementType.TYPE )
    @Retention( RetentionPolicy.RUNTIME )
    @Repeatable( Filters.class )
    public @interface Filter {
        String value();
    };
 
    @Filter( "filter1" )
    @Filter( "filter2" )
    public interface Filterable {        
    }
 
    public static void main(String[] args) {
        for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
            System.out.println( filter.value() );
        }
    }
}

复制代码
正如我们所见,这里的Filter类使用@Repeatable(Filters.class)注解修饰,而Filters是存放Filter注解的容器,编译器尽量对开发者屏蔽这些细节。这样,Filterable接口可以用两个Filter注解注释(这里并没有提到任何关于Filters的信息)。

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

filter1
filter2

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

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

复制代码

package com.javacodegeeks.java8.type.inference;
 
public class Value< T > {
    public static< T > T defaultValue() { 
        return null; 
    }
 
    public T getOrDefault( T value, T defaultValue ) {
        return ( value != null ) ? value : defaultValue;
    }
}

复制代码
下列代码是Value类型的应用:

复制代码

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()。

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

复制代码

package com.javacodegeeks.java8.annotations;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
 
public class Annotations {
    @Retention( RetentionPolicy.RUNTIME )
    @Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
    public @interface NonEmpty {        
    }
 
    public static class Holder< @NonEmpty T > extends @NonEmpty Object {
        public void method() throws @NonEmpty Exception {            
        }
    }
 
    @SuppressWarnings( "unused" )
    public static void main(String[] args) {
        final Holder< String > holder = new @NonEmpty Holder< String >();        
        @NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>();        
    }
}

复制代码
ElementType.TYPE_USER和ElementType.TYPE_PARAMETER是Java 8新增的两个注解,用于描述注解的使用场景。Java 语言也做了对应的改变,以识别这些新增的注解。

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

复制代码

package com.javacodegeeks.java8.parameter.names;
 
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
 
public class ParameterNames {
    public static void main(String[] args) throws Exception {
        Method method = ParameterNames.class.getMethod( "main", String[].class );
        for( final Parameter parameter: method.getParameters() ) {
            System.out.println( "Parameter: " + parameter.getName() );
        }
    }
}

复制代码
在Java 8中这个特性是默认关闭的,因此如果不带-parameters参数编译上述代码并运行,则会输出如下结果:

Parameter: arg0

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

Parameter: args

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

复制代码

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
        <compilerArgument>-parameters</compilerArgument>
        <source>1.8</source>
        <target>1.8</target>
    </configuration>
</plugin>

复制代码
4. Java官方库的新特性
Java 8增加了很多新的工具类(date/time类),并扩展了现存的工具类,以支持现代的并发编程、函数式编程等。

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

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

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

Optional< String > fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );        
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) ); 
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );

如果Optional实例持有一个非空值,则isPresent()方法返回true,否则返回false;orElseGet()方法,Optional实例持有null,则可以接受一个lambda表达式生成的默认值;map()方法可以将现有的Opetional实例的值转换成新的值;orElse()方法与orElseGet()方法类似,但是在持有null的时候返回传入的默认值。

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

Full Name is set? false
Full Name: [none]
Hey Stranger!

再看下另一个简单的例子:

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

这个例子的输出是:

First Name is set? true
First Name: Tom
Hey Tom!

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

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

复制代码

public class Streams  {
    private enum Status {
        OPEN, CLOSED
    };
 
    private static final class Task {
        private final Status status;
        private final Integer points;
 
        Task( final Status status, final Integer points ) {
            this.status = status;
            this.points = points;
        }
 
        public Integer getPoints() {
            return points;
        }
 
        public Status getStatus() {
            return status;
        }
 
        @Override
        public String toString() {
            return String.format( "[%s, %d]", status, points );
        }
    }
}

复制代码
Task类有一个分数(或伪复杂度)的概念,另外还有两种状态:OPEN或者CLOSED。现在假设有一个task集合:

final Collection< Task > tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 ) 
);

首先看一个问题:在这个task集合中一共有多少个OPEN状态的点?在Java 8之前,要解决这个问题,则需要使用foreach循环遍历task集合;但是在Java 8中可以利用steams解决:包括一系列元素的列表,并且支持顺序和并行处理。

复制代码

// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
    .stream()
    .filter( task -> task.getStatus() == Status.OPEN )
    .mapToInt( Task::getPoints )
    .sum();
 
System.out.println( "Total points: " + totalPointsOfOpenTasks );

复制代码
运行这个方法的控制台输出是:

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

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

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

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

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

复制代码

// Calculate total points of all tasks
final double totalPoints = tasks
   .stream()
   .parallel()
   .map( task -> task.getPoints() ) // or map( Task::getPoints ) 
   .reduce( 0, Integer::sum );
 
System.out.println( "Total points (all tasks): " + totalPoints );

复制代码
这里我们使用parallel方法并行处理所有的task,并使用reduce方法计算最终的结果。控制台输出如下:

Total points(all tasks): 26.0

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

// Group tasks by their status
final Map< Status, List< Task > > map = tasks
    .stream()
    .collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );

控制台的输出如下:

{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}

最后一个关于tasks集合的例子问题是:如何计算集合中每个任务的点数在集合中所占的比重,具体处理的代码如下:

复制代码

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

复制代码
控制台输出结果如下:

[19%, 50%, 30%]

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

final Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
    lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}

Stream的方法onClose 返回一个等价的有额外句柄的Stream,当Stream的close()方法被调用的时候这个句柄会被执行。Stream API、Lambda表达式还有接口默认方法和静态方法支持的方法引用,是Java 8对软件开发的现代范式的响应。

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

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

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


// Get the system clock as UTC offset 
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );

这个例子的输出结果是:

2014-04-12T15:19:29.282Z
1397315969360

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

复制代码

// Get the local date and local time
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
 
System.out.println( date );
System.out.println( dateFromClock );
 
// Get the local date and local time
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
 
System.out.println( time );
System.out.println( timeFromClock );

复制代码
上述例子的输出结果如下:

2014-04-12
2014-04-12
11:25:54.568
15:25:54.568

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

// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
 
System.out.println( datetime );
System.out.println( datetimeFromClock );

上述这个例子的输出结果如下:

2014-04-12T11:37:52.309
2014-04-12T15:37:52.309

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

复制代码

// Get the zoned date/time
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
 
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );

复制代码
这个例子的输出结果是:

2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]

最后看下Duration类,它持有的时间精确到秒和纳秒。这使得我们可以很容易得计算两个日期之间的不同,例子代码如下:

复制代码

/ Get duration between two dates
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );
 
final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );

复制代码
这个例子用于计算2014年4月16日和2015年4月16日之间的天数和小时数,输出结果如下:

Duration in days: 365
Duration in hours: 8783

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

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

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
 
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );

这个代码的输出结果如下:

jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
4.5 Base64

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

复制代码

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

复制代码
这个例子的输出结果如下:

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

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

复制代码

package com.javacodegeeks.java8.parallel.arrays;
 
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
 
public class ParallelArrays {
    public static void main( String[] args ) {
        long[] arrayOfLong = new long [ 20000 ];        
 
        Arrays.parallelSetAll( arrayOfLong, 
            index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
            i -> System.out.print( i + " " ) );
        System.out.println();
 
        Arrays.parallelSort( arrayOfLong );        
        Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 
            i -> System.out.print( i + " " ) );
        System.out.println();
    }
}

复制代码
上述这些代码使用parallelSetAll()方法生成20000个随机数,然后使用parallelSort()方法进行排序。这个程序会输出乱序数组和排序数组的前10个元素。上述例子的代码输出的结果是:

Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378 
Sorted: 39 220 263 268 325 607 655 678 723 793

4.7 并发性
基于新增的lambda表达式和steam特性,为Java 8中为java.util.concurrent.ConcurrentHashMap类添加了新的方法来支持聚焦操作;另外,也为java.util.concurrentForkJoinPool类添加了新的方法来支持通用线程池操作(更多内容可以参考我们的并发编程课程)。

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

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

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

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

function f() { 
     return 1; 
}; 
 
print( f() + 1 );

可以在命令行中执行这个命令:jjs func.js,控制台输出结果是:

1
2

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

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

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

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

复制代码

org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io                                            
      -> java.lang                                          
      -> java.lang.annotation                               
      -> java.lang.ref                                      
      -> java.lang.reflect                                  
      -> java.util                                          
      -> java.util.concurrent                               
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang                                          
      -> java.lang.annotation                               
      -> java.lang.reflect                                  
      -> java.util

复制代码
更多的细节可以参考官方文档。

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

  • 什么是竞态条件? 举个例子说明

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。

导致竞态条件发生的代码区称作临界区。
在临界区中使用适当的同步就可以避免竞态条件。
临界区实现方法有两种,一种是用synchronized,一种是用Lock显式锁实现。

class Counter { 
protected long count = 0; 
public void add(long value) { 
this.count = this.count + value; 
} 
}

观察线程A和B交错执行会发生什么,两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。
add()方法就是一个临界区,它会产生竞态条件。

  • JRE、JDK、JVM 及 JIT 之间有什么不同
定义

JDK:

Java Develpment Kit java 开发工具

JRE:

Java Runtime Environment java运行时环境

JVM:

java Virtual Machine java 虚拟机

使用人群

jdk 是给开发人员使用的, jre和jvm是给普通用户使用

详细介绍

JDK:
是java开发工具包,是Sun公司针对java开发人员的产品,jdk中包含jre,在jdk的安装目录下有一个jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里面就是jvm,lib中是java工作需要的类库,而java和lib合起来就称为jre
JDK(Java Development Kit) 是整个JAVA的核心,包括了Java运行环境(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。
基本上每个学java的人都会先在机器 上装一个JDK,那他都包含哪几部分呢?在目录下面有 六个文件夹、一个src类库源码压缩包、和其他几个声明文件。其中,真正在运行java时起作用的 是以下四个文件夹:bin、include、lib、 jre。有这样一个关系,JDK包含JRE,而JRE包 含JVM。
bin:最主要的是编译器(javac.exe)
include:java和JVM交互用的头文件
lib:类库
jre:java运行环境
(注意:这里的bin、lib文件夹和jre里的bin、lib是 不同的)
总的来说JDK是用于java程序的开发,而jre则是只能运行class而没有编译的功能。
JRE:
是运行基于java语言编写的程序所不可缺少的运行环境,也是通过它,java的开发者才可以将自己开发的程序发布到用户手中,让用户使用,与大家熟悉的jdk不同,jre是运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器,调试器)等,知识针对使用java程序的用户
JRE(Java Runtime Environment,Java运行环境),包含JVM标准实现及Java核心类库。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)
JRE是指java运行环境。光有JVM还不能成class的 执行,因为在解释class的时候JVM需要调用解释所需要的类库lib。 (jre里有运行.class的java.exe)
JRE ( Java Runtime Environment ),是运行 Java 程序必不可少的(除非用其他一些编译环境编译成.exe可执行文件……),JRE的 地位就象一台PC机一样,我们写好的Win64应用程序需要操作系统帮 我们运行,同样的,我们编写的Java程序也必须要JRE才能运行。
JVM:
就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序首先被编译为.class文件,这种类文件可以在虚拟机上运行,也就是说class并不直接与机器的操作系统相对应。
而是经过虚拟机间接的与操作系统交互,由虚拟机将程序解释给本地系统执行,只有jvm还不能将class执行,因为在解释class的时候jvm需要调用解释所需要的类库lib,而jre包含lib类库,jvm屏蔽了具体操作系统平台的相关信息,使得java程序只需要生成在java虚拟机上运行的目标代码。可以在多种平台(操作系统)上不加修改的运行

关联关系

简单来说就是JDK包含JRE,JRE包含JVM的关系
Java基础:JDK、JRE、JVM的区别与联系
JVM就是我们常说的java虚拟机,它是整个java实现跨平台的 最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可 以在虚拟机上执行,也就是说class并不直接与机器的…
三者联系:JVM不能单独搞定class的执行,解释class的时候JVM需要调用解释所需要的类库lib。在JDK下面的的jre目录里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
JVM+Lib=JRE。总体来说就是,我们利用JDK(调用JAVA API)开发了属于我们自己的JAVA程序后,通过JDK中的编译程序(javac)将我们的文本java文件编译成JAVA字节码,在JRE上运行这些JAVA字节码,JVM解析这些字节码,映射到CPU指令集或OS的系统调用。
a.JDK和JRE区别:在bin文件夹下会发现,JDK有javac.exe而JRE里面没有,javac指令是用来将java文件编译成class文件的,这是开发者需要的,而用户(只需要运行的人)是不需要的。JDK还有jar.exe, javadoc.exe等等用于开发的可执行指令文件。这也证实了一个是开发环境,一个是运行环境。
b.JRE和JVM区别:JVM并不代表就可以执行class了,JVM执行.class还需要JRE下的lib类库的支持,尤其是rt.jar。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值