经过多种渠道的搜集,对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情况总结如下: