Java编程热门问题总结——编程技巧篇

来源:互联网 发布:韩国ip地址和端口 编辑:程序博客网 时间:2024/06/05 17:58

  经过多种渠道的搜集,对Java程序员在编程过程中常见的问题及解答作一个整理。

1、去掉烦人的“!=null”(判空语句)

  为了避免空指针调用,我们经常会看到这样的语句:

if (someobject != null) {    someobject.doCalc();}

  最终,项目中会存在大量判空代码,多么丑陋繁冗!如何避免这种情况?我们是否滥用了判空呢?

  这是初、中级程序猿经常会遇到的问题。他们总喜欢在方法中返回null,因此,在调用这些方法时,也不得不去判空。另外,也许受此习惯影响,他们总潜意识地认为,所有的返回都是不可信任的,为了保护自己程序,就加了大量的判空。

  回到这个问题本身,进行判空前,请区分以下两种情况:
  1、null是一个有效有意义的返回值
  2、null是无效有误的返回值

  接下来将详细讨论这两种情况。先说第2种情况,null就是一个不合理的参数,就应该明确地中断程序,往外抛错误。这种情况常见于API方法。例如你开发了一个接口,id是一个必选的参数,如果调用方没传这个参数给你,当然不行。你要感知到这个情况,告诉调用方“嘿,哥们,你传个null给我做甚”。

  在第2种情况下,相对于判空语句,更好的检查方式有两个:
  (1)assert语句,你可以把错误原因放到assert的参数中,这样不仅能保护你的程序不往下走,而且还能把错误原因返回给调用方,岂不是一举两得。
  (2)也可以直接抛出空指针异常。上面说了,此时null是个不合理的参数,有问题就是有问题,就应该大大方方往外抛异常

  第1种情况会更复杂一些。 这种情况下,null是个”看上去“合理的值,例如,我查询数据库,某个查询条件下,就是没有对应值,此时null算是表达了“空”的概念。

  这里给一些实践建议:
   (1)假如方法的返回类型是collections,当返回结果是空时,你可以返回一个空的collections,而不要返回null。这样调用侧就能大胆地处理这个返回,例如调用侧拿到返回后,可以直接print list.size(),又无需担心空指针问题。(代码习惯很重要!如果你养成习惯,都是这样写代码返回空collections而不返回null,你调用自己写的方法时,就能大胆地忽略判空)
  (2)返回类型不是collections,又怎么办呢? 那就返回一个空对象(而非null对象),下面举个“栗子”,假设有如下代码:

public interface Action {  void doSomething();}public interface Parser {  Action findAction(String userInput);}

  其中,Parse接口有一个方法FindAction,这个方法会依据用户的输入,找到并执行对应的动作。假如用户输入不对,可能就找不到对应的动作(Action),因此findAction就会返回null,接下来action调用doSomething方法时,就会出现空指针, 解决这个问题的一个方式,就是使用Null Object pattern(空对象模式)。

  我们来改造一下。实现Parse接口的类定义如下,这样定义findAction方法后,确保无论用户输入什么,都不会返回null对象:

public class MyParser implements Parser {  private static Action DO_NOTHING = new Action() {        @Override        public void doSomething() {             /* do nothing */         }  };  @Override  public Action findAction(String userInput) {    if ( /* we can't find any actions */ ) {      return DO_NOTHING;    }  }}

  对比下面两份调用实例:

  (1)冗余:每获取一个对象,就判一次空

Parser parser = ParserFactory.getParser();if (parser == null) {  // now what?}Action action = parser.findAction(someInput);if (action == null) {  // do nothing} else {  action.doSomething();}

  (2)精简

ParserFactory.getParser().findAction(someInput).doSomething();

  因为无论什么情况,都不会返回空对象,因此通过findAction拿到action后,可以放心地调用action的doSomething方法。

  总而言之,如果你想返回null,请停下来想一想,这个地方是否更应该抛出一个异常

2、输出Java数组最简单的方式

  因为 Java 数组中没有toString()方法,所以我如果直接调用数组toString()方法的话,只会得到它的内存地址。像这样,显得并不人性化:

int[] intArray = new int[] {1, 2, 3, 4, 5};System.out.println(intArray);     // 有时候会输出 '[I@3343c8b3'

  在 Java 5+ 以上中使用 Arrays.toString(arr) 或 Arrays.deepToString(arr)来打印(输出)数组。不要忘了import java.util.Arrays;

import java.util.Arrays;int[] intArray = new int[] {1, 2, 3, 4, 5};System.out.println(Arrays.toString(intArray));//输出: [1, 2, 3, 4, 5]String[] strArray = new String[] {"John", "Mary", "Bob"};System.out.println(Arrays.deepToString(strArray));*//输出: [John, Mary, Bob]

  Arrays.deepToString与Arrays.toString方法的不同之处在于,Arrays.deepToString更适合打印多维数组。例如:

String[][] b = new String[3][4];for (int i = 0; i < 3; i++) {    for (int j = 0; j < 4; j++) {        b[i][j] = "A" + j;    }} System.out.println(Arrays.toString(b));//输出[[Ljava.lang.String;@55e6cb2a, [Ljava.lang.String;@23245e75, [Ljava.lang.String;@28b56559]System.out.println(Arrays.deepToString(b));//输出[[A0, A1, A2, A3], [A0, A1, A2, A3], [A0, A1, A2, A3]]

3、如何最快地初始化一个ArrayList

  为了测试,我需要临时快速创建一个ArrayList。一开始我这样做:

ArrayList<String> places = new ArrayList<String>();places.add("Buenos Aires");places.add("Córdoba");places.add("La Plata");

  经过重构优化后,一行代码就可以创建一个ArrayList并初始化之:

ArrayList<String> places = new ArrayList<String>(    Arrays.asList("Buenos Aires", "Córdoba", "La Plata"));

  还有另一种方式,写一个匿名内部类,然后在其中做初始化(也被称为 brace initialization):

ArrayList<String> list = new ArrayList<String>() {{    add("A");    add("B");    add("C");}};

4、实现Runnable接口还是继承Thread类

  在Java中,并发执行任务一般有两种方式:(1)实现Runnable接口 (2)继承Thread类。

  一般而言,推荐使用方式(1),主要是由于大多数情况下,人们并不会特别去关注线程的行为,也不会去改写Thread类已有的行为或方法,仅仅是期望执行任务而已。 因此,使用接口的方式能避免引入一些并不需要的东西,同时也不会影响继承其他类,并使程序更加灵活。

  Runnable与Thread不是对等的概念 在《Thinking in Java》中,作者吐槽过Runnable的命名,称其叫做Task更为合理。 在Java中,Runnable只是一段用于描述任务的代码段而已,是静态的概念,需要通过线程来执行。而Thread更像是一个活体,自身就具有很多行为,能够用来执行任务

  1、仅仅当你确实想要重写(override)一些已有行为时,才使用继承,否则请使用接口。
  2、在Java 5之前,创建了Thread却没调用其start()方法,可能导致内存泄露。

5、LinkedList、ArrayList各自的使用场景,如何确定该用哪一个

  一言以蔽之,在大部分情况下,使用ArrayList会好一些。

  二者在耗时上各有优缺点。ArrayList稍有优势,List只是一个接口,而LinkedList、ArrayList是List接口的不同实现。LinkedList的模型是双向链表,而ArrayList则是动态数组

  首先对比一下常用操作的算法复杂度,LinkedList:
  - get(int index) : O(n)
  - add(E element) : O(1)
  - add(int index, E element) : O(n)
   - remove(int index) : O(n)
   - Iterator.remove() : O(1) —-LinkedList的主要优点
   - ListIterator.add(E element) :O(1) —- LinkedList的主要优点

  ArrayList:
  - get(int index) :O(1)—- ArrayList的主要优点
  - add(E element) : 基本是O(1) , 因为动态扩容的关系,最差时是 O(n)
  - add(int index, E element) : 基本是O( n - index) , 因为动态扩容的关系,最差时是 O(n)
   - remove(int index) : O(n - index)
   - Iterator.remove(): O(n - index)
   - ListIterator.add(E element) :O(n - index)

  LinkedList,因为本质上是个链表,所以通过Iterator来插入和移除操作的耗时,都是个恒量,但如果要获取某个位置的元素,则要做指针遍历。因此,get操作的耗时会跟List长度有关。

  对于ArrayList来说,得益于快速随机访问的特性,获取任意位置元素的耗时是常量级的。但是,如果是add或者remove操作,要分两种情况,如果是在尾部做add,也就是执行add方法(没有index参数),此时不需要移动其他元素,耗时是O(1),但如果不是在尾部做add,也就是执行add(int index, E element),这时候在插入新元素的同时,也要移动该位置后面的所有元素来为新元素腾出位置,此时耗时是O(n-index)。另外,当List长度超过初始化容量时,会自动生成一个新的array(长度是之前的1.5倍),此时会将旧的array移动到新的array上,这种情况下的耗时是O(n)。

  总而言之,get操作,ArrayList快一些。而add操作,两者差不多(除非是你希望在List中间插入节点,且维护了一个Iterator指向指定位置,这时候linkedList能快一些,但是我们更多时候是直接在尾部插入节点,这种特例的情况并不多)。

  空间占用上,ArrayList完胜,看下面两者的内存占用图:

  这三个图,横轴是list长度,纵轴是内存占用值。两条蓝线是LinkedList,两条红线是ArrayList。

  可以看到,LinkedList的空间占用,要远超ArrayList。LinkedList的线更陡,随着List长度的扩大,所占用的空间要比同长度的ArrayList大得多。 注:从mid JDK6之后,默认启用了CompressedOops ,因此64位及32位下的结果没有差异,LinkedList x64和LinkedList x32的线是一样的。

6、StringBuilder和StringBuffer有哪些区别呢

  最主要的区别,StringBuffer的实现用了synchronized(锁),而StringBuilder没有。因此,StringBuilder会比StringBuffer快。

  如果你
  1、非常非常追求性能(其实两个都不慢,比直接操作String要快非常多了)
  2、不需要考虑线程安全问题
  3、JRE是1.5+

  可以用StringBuilder,反之,请用StringBuffer。

  性能测试例子。如下这个例子,使用StringBuffer,耗时2241ms,而StringBuilder是753ms。

public class Main {    public static void main(String[] args) {        int N = 77777777;        long t;        {            StringBuffer sb = new StringBuffer();            t = System.currentTimeMillis();            for (int i = N; i --> 0;) {                sb.append("");            }            System.out.println(System.currentTimeMillis() - t);        }        {            StringBuilder sb = new StringBuilder();            t = System.currentTimeMillis();            for (int i = N; i --> 0;) {                sb.append("");            }            System.out.println(System.currentTimeMillis() - t);        }    }}

7、怎样创建一个文件并向该文件写文本内容

  创建一个文本文件(注意:如果该文件已经存在,则会覆盖该文件)

PrintWriter writer = new PrintWriter("the-file-name.txt", "UTF-8");writer.println("The first line");writer.println("The second line");writer.close();

  创建一个二进制文件(同样会覆盖已经存在的同名文件)

byte data[] = ...FileOutputStream out = new FileOutputStream("the-file-name");out.write(data);out.close();

  Java 7+ 用户可以用File类来写文件 创建一个文本文件:

List<String> lines = Arrays.asList("The first line", "The second line");Path file = Paths.get("the-file-name.txt");Files.write(file, lines, Charset.forName("UTF-8"));

  创建一个二进制文件:

byte data[] = ...Path file = Paths.get("the-file-name");Files.write(file, data);

  如果已经有想要写到文件中的内容,java.nio.file.Files 作为 Java 7 附加部分的native I/O,提供了简单高效的方法来实现目标。基本上创建文件,写文件只需要一行,而且只需一个方法调用! 下面的例子创建并且写了6个不同的文件来展示是怎么使用的。

Charset utf8 = StandardCharsets.UTF_8;List<String> lines = Arrays.asList("1st line", "2nd line");byte[] data = {1, 2, 3, 4, 5};try {    Files.write(Paths.get("file1.bin"), data);    Files.write(Paths.get("file2.bin"), data,StandardOpenOption.CREATE, StandardOpenOption.APPEND);    Files.write(Paths.get("file3.txt"), "content".getBytes());    Files.write(Paths.get("file4.txt"), "content".getBytes(utf8));    Files.write(Paths.get("file5.txt"), lines, utf8);    Files.write(Paths.get("file6.txt"), lines, utf8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);} catch (IOException e) {    e.printStackTrace();}

  下面是一个小程序来创建和写文件。该版本的代码比较长,但是可以容易理解。

import java.io.BufferedWriter;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.OutputStreamWriter;import java.io.Writer;public class writer {    public void writing() {        try {            //Whatever the file path is.            File statText = new File("E:/Java/Reference/bin/images/statsTest.txt");            FileOutputStream is = new FileOutputStream(statText);            OutputStreamWriter osw = new OutputStreamWriter(is);                Writer w = new BufferedWriter(osw);            w.write("POTATO!!!");            w.close();        } catch (IOException e) {            System.err.println("Problem writing to the file statsTest.txt");        }    }    public static void main(String[]args) {        writer write = new writer();        write.writing();    }}

8、如何对一组对象进行排序?

  我们如何对一组对象进行排序?如果我们需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象列表,我们可以使用Collection.sort()方法

   两个类都有用于自然排序(使用Comparable)或基于标准的排序(使用Comparator)的重载方法sort()。

  Collections内部使用数组排序方法,所有它们两者都有相同的性能,只是Collections需要花时间将列表转换为数组。

9、如何避免在JSP文件中使用Java代码

  在Java EE中,类似如下的三行代码:

<%= x+1 %><%= request.getParameter("name") %><%! counter++; %>

  这三行代码是学校教的老式代码。在JSP1.2规范中,存在一些方法可以避免在JSP文件中使用Java代码。在JSP1.2中应该如何避免使用Java代码呢?

  在大约十年前,taglibs(比如JSTL)和EL(EL表达式,${})诞生的时候,在JSP中使用scriptlets(类似<% %>)这种做法,就确实已经是不被鼓励使用的做法了

  scriptlets 主要的缺点有:
  1、重用性 :你不可以重用scriptlets
  2、可替换性 :你不可以让scriptlets抽象化
  3、面向对象能力 :你不可以使用继承或组合
  4、调试性 :如果scriptlets中途抛出了异常,你只能获得一个空白页
  5、可测试性 :scriptlets不能进行单元测试
  6、可维护性 :(这句有些词语不确定)需要更多的时间去维护混合的/杂乱的/冲突的代码逻辑

  Oracle自己也在 JSP coding conventions一文中推荐在功能可以被标签库所替代的时候避免使用scriptlets语法。以下引用它提出的几个观点:

  (1)在JSP 1.2规范中,强烈推荐使用JSTL来减少JSP scriptlets语法的使用。一个使用JSTL的页面,总得来说会更加地容易阅读和维护。

  (2)在任何可能的地方,当标签库能够提供相同的功能时,尽量避免使用JSP scriptlets语法。这会让页面更加容易阅读和维护,帮助将业务逻辑从表现层逻辑中分离,也会让页面往更符合JSP 2.0风格的方向发展(JSP 2.0规范中,支持但是极大弱化了JSP scriptlets语法)

  (3)本着适应模型-显示层-控制器(MVC)设计模式中关于减少业务逻辑层与显示层之间的耦合的精神,JSP scriptlets语法不应该被用来编写业务逻辑。相应的,JSP scriptlets语法应该只在传送一些服务端返回的处理客户端请求的数据(也称为value objects)的时候会被使用,尽管如此,使用一个controller servlet来处理或者用JSTL标签库来做这些事会更好

  如何替换scriptlets语句,取决于代码/逻辑的目的。更常见的是,被替换的语句会被放在另外的一些更值得放的Java类里。

  如果你想在每个请求、每个页面请求都运行相同的Java代码,比如说 检查一个用户是否在登录状态,就要实现一个 过滤器,在doFilter()方法中编写正确的代码,例如:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {    if (((HttpServletRequest) request).getSession().getAttribute("user") == null) {        ((HttpServletResponse) response).sendRedirect("login");         // Not logged in, redirect to login page.    } else {        chain.doFilter(request, response);         // Logged in, just continue request.    }}

  如果你想执行一些Java代码来预处理一个请求,例如,预加载某些从数据库加载的数据来显示在一些表格里,可能还会有一些查询参数,那么可以实现一个Servlet,在doGet()方法里编写正确的代码,例如:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {    try {        List<Product> products = productService.list(); // Obtain all products.        request.setAttribute("products", products);         // Store products in request scope.        request.getRequestDispatcher("/WEB-INF/products.jsp").forward(request, response);         // Forward to JSP page to display them in a HTML table.    } catch (SQLException e) {        throw new ServletException("Retrieving products failed!", e);    }}

  这个方法能够更方便地处理异常。这样会在渲染、展示JSP页面时访问数据库。在数据库抛出异常的时候,你可以根据情况返回不同的响应或页面。在上面的例子,出错时默认会展示500页面,你也可以改变web.xml的<error-page>来自定义异常处理错误页。

  如果你想执行一些Java代码来后置处理(postprocess)一个请求,例如处理表单提交,那么实现一个Servlet,在doPost()里写上正确的代码。例如:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {    String username = request.getParameter("username");    String password = request.getParameter("password");    User user = userService.find(username, password);    if (user != null) {        request.getSession().setAttribute("user", user); // Login user.        response.sendRedirect("home"); // Redirect to home page.    } else {        request.setAttribute("message", "Unknown username/password. Please retry."); // Store error message in request scope.        request.getRequestDispatcher("/WEB-INF/login.jsp").forward(request, response);         // Forward to JSP page to redisplay login form with error.    }}

  这个处理不同目标结果页的方法会比原来更加简单:可以显示一个带有表单验证错误提示的表单(在这个特别的例子中,你可以用EL表达式${message}来显示错误提示),或者仅仅跳转到成功的页面。

  如果你想执行一些Java代码来控制执行计划或让request和response跳转目标,可以用MVC模式实现一个Servlet,例如:

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {    try {        Action action = ActionFactory.getAction(request);        String view = action.execute(request, response);        if (view.equals(request.getPathInfo().substring(1)) {            request.getRequestDispatcher("/WEB-INF/" + view +                 ".jsp").forward(request, response);        } else {            response.sendRedirect(view);        }    } catch (Exception e) {        throw new ServletException("Executing action failed.", e);    }}

  如果你想执行一些Java代码来控制JSP页面的数据渲染流程,那么你需要使用一些(已经存在的)流程控制标签库,比如JSTL core,例如,在一个表格显示List<Product>。

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>...<table>    <c:forEach items="${products}" var="product">        <tr>            <td>${product.name}</td>            <td>${product.description}</td>            <td>${product.price}</td>        </tr>    </c:forEach></table>

  相比于杂乱无章的scriptlets 的分支大括号,这些XML风格的标签可以很好地适应HTML代码,代码变得更好阅读,也因此更好地维护

  下面这个简单的设置可以配置你的Web程序,让其在使用scriptlets 的时候自动抛出异常。

<jsp-config>    <jsp-property-group>        <url-pattern>*.jsp</url-pattern>        <scripting-invalid>true</scripting-invalid>    </jsp-property-group></jsp-config>

  如果你想执行一些Java代码来在JSP中访问和显示一些“后端”数据,你需要使用EL(表达式),${}。例如,显示已经提交了的数值:

<input type="text" name="foo" value="${param.foo}" />

  ${param.foo}会显示request.getParameter(“foo”)这条语句的输出结果。

  如果你想在JSP直接执行一些工具类的Java代码(典型的一些public static方法),你需要定义它,并使用EL表达式函数。这是JSTL里的标准函数标签库,但是你也可以轻松地创建自己需要的功能,下面是一个使用fn:escapeXml来避免XSS攻击的例子。

<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>...<input type="text" name="foo" value="${fn:escapeXml(param.foo)}" />

  注意,XSS并不是Java/JSP/JSTL/EL/任何技术相关的东西,这个问题是任何Web应用程序都需要关心的问题,scriptlets 并没有为这个问题提供良好的解决方案,至少没有标准的Java API的解决方案。JSP的继承者Facelets内含了HTML转义功能,所以在Facelets里你不用担心XSS攻击的问题。

10、Set里的元素是不能重复的,那么用什么方法来区分重复与否呢, 是用==还是equals()?

  什么是Set?

  Set是Collection容器的一个子接口,它不允许出现重复元素,当然也只允许有一个null对象

  JPI中写的很明白:Set不包含满足e1.equals(e2)的元素对e1和e2,由此可见回答使用equals()区分更合适。应该从equals()和==的区别谈起,==是用来判断两者是否是同一对象(同一事物)的,而equals()是用来判断两者是否引用了同一个对象。

  再看一下Set里面存的是对象,还是对象的引用。根据Java的存储机制可知,Set里面存放的是对象的引用,所以当两个元素只要满足了equals()时就已经表示两者指向了同一个对象,也就出现了重复元素。所以应该用equals()来判断

  Set是Java中一个不包含重复元素的collection。更正式地说,Set 不包含满足 e1.equals(e2) 的元素对e1和e2,并且最多包含一个null元素。正如其名称所暗示的,此接口模仿了数学上的Set抽象。

11、利用Set中元素的唯一性,快速对另一个集合去重,避免使用List的contains方法进行遍历去重

  如果需要对List集合中的重复值进行处理,大部分是采用两种方法,一种是用遍历List集合判断后赋给另一个List集合,一种是将List先赋给Set集合再返回给List集合。

public class SetRemoveDuplication {    public static void test1(){        List<String> list  =   new ArrayList<String>();        list.add("aaa");        list.add("bbb");        list.add("aaa");        list.add("aba");        list.add("aaa");        Set<String> set = new HashSet<String>();        List<String> newList = new ArrayList<String>();        set.addAll(list);        newList.addAll(set);        System.out.println( "去重后的集合: " + newList);    }    //set去重(缩减为一行)    public static void test2(){        List<String> list  =   new ArrayList<String>();        list.add("aaa");        list.add("bbb");        list.add("aaa");        list.add("aba");        list.add("aaa");        List<String> newList = new ArrayList<String>(new HashSet<String>(list));        System.out.println( "去重后的集合: " + newList);    }    /**     * hashset不进行排序,还有一种方法是用treeset,去重并且按照自然顺序排列,     * 将hashset改为treeset就可以了(原本的顺序是改变的,只是按照字母表顺序排列而已)     *     */    public static void test3(){        List<String> list  =   new ArrayList<String>();        list.add("aaa");        list.add("bbb");        list.add("aaa");        list.add("aba");        list.add("aaa");        List<String> newList = new ArrayList<String>(new TreeSet<String>(list));        System.out.println( "去重后的集合: " + newList);    }    //遍历后判断赋给另一个list集合    public static void test4(){        List<String> list  =   new  ArrayList<String>();        list.add("aaa");        list.add("bbb");        list.add("aaa");        list.add("aba");        list.add("aaa");        List<String> newList = new  ArrayList<String>();        for (String cd : list) {            if(!newList.contains(cd)){                newList.add(cd);            }        }        System.out.println( "去重后的集合: " + newList);    }    public static void main(String[] args) {        System.out.println("////////////////////////////");        SetRemoveDuplication.test1();        System.out.println("////////////////////////////");        SetRemoveDuplication.test2();        System.out.println("////////////////////////////");        SetRemoveDuplication.test3();        System.out.println("////////////////////////////");        SetRemoveDuplication.test4();        System.out.println("////////////////////////////");    }}

  运行结果:

////////////////////////////去重后的集合: [aaa, bbb, aba]////////////////////////////去重后的集合: [aaa, aba, bbb]////////////////////////////去重后的集合: [aaa, aba, bbb]////////////////////////////去重后的集合: [aaa, aba, bbb]////////////////////////////去重后的集合: [aaa, bbb, aba]////////////////////////////

  合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。稳定性是指集合每次遍历的元素次序是一定的,有序性是指遍历的结果是按某种比较规则依次排列的。如ArrayList是order/unsort;HashMap是unorder/unsort;TreeSort是order/sort

12、Map类集合哪些实现的K/V能存储null ?

  常用的几个map接口实现类的K/V存储null情况总结如下: