Java null最佳实践
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java null最佳实践相关的知识,希望对你有一定的参考价值。
上一篇:近5年常考Java面试题及答案整理(二)
68、Java中如何实现序列化,有什么意义?
答:序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆(可以参考第29题)。
69、Java中有几种类型的流?
答:字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在 java.io
包中还有许多其他的流,主要是为了提高性能和使用方便。关于Java的I/O需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外Java中的流不同于C#的是它只有一个维度一个方向。
面试题 - 编程实现文件拷贝。(这个题目在笔试的时候经常出现,下面的代码给出了两种实现方案)
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.io.OutputStream; 6 import java.nio.ByteBuffer; 7 import java.nio.channels.FileChannel; 8 9 public final class MyUtil { 10 11 private MyUtil() { 12 throw new AssertionError(); 13 } 14 15 public static void fileCopy(String source, String target) throws IOException { 16 try (InputStream in = new FileInputStream(source)) { 17 try (OutputStream out = new FileOutputStream(target)) { 18 byte[] buffer = new byte[4096]; 19 int bytesToRead; 20 while((bytesToRead = in.read(buffer)) != -1) { 21 out.write(buffer, 0, bytesToRead); 22 } 23 } 24 } 25 } 26 27 public static void fileCopyNIO(String source, String target) throws IOException { 28 try (FileInputStream in = new FileInputStream(source)) { 29 try (FileOutputStream out = new FileOutputStream(target)) { 30 FileChannel inChannel = in.getChannel(); 31 FileChannel outChannel = out.getChannel(); 32 ByteBuffer buffer = ByteBuffer.allocate(4096); 33 while(inChannel.read(buffer) != -1) { 34 buffer.flip(); 35 outChannel.write(buffer); 36 buffer.clear(); 37 } 38 } 39 } 40 } 41 }
注意:上面用到Java 7的TWR,使用TWR后可以不用在finally中释放外部资源 ,从而让代码更加优雅。
70、写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。
答:代码如下:
1 import java.io.BufferedReader; 2 import java.io.FileReader; 3 4 public final class MyUtil { 5 6 // 工具类中的方法都是静态方式访问的因此将构造器私有不允许创建对象(绝对好习惯) 7 private MyUtil() { 8 throw new AssertionError(); 9 } 10 11 /** 12 * 统计给定文件中给定字符串的出现次数 13 * 14 * @param filename 文件名 15 * @param word 字符串 16 * @return 字符串在文件中出现的次数 17 */ 18 public static int countWordInFile(String filename, String word) { 19 int counter = 0; 20 try (FileReader fr = new FileReader(filename)) { 21 try (BufferedReader br = new BufferedReader(fr)) { 22 String line = null; 23 while ((line = br.readLine()) != null) { 24 int index = -1; 25 while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) { 26 counter++; 27 line = line.substring(index + word.length()); 28 } 29 } 30 } 31 } catch (Exception ex) { 32 ex.printStackTrace(); 33 } 34 return counter; 35 } 36 37 }
71、如何用Java代码列出一个目录下所有的文件?
答:
如果只要求列出当前文件夹下的文件,代码如下所示:
1 import java.io.File; 2 3 class Test12 { 4 5 public static void main(String[] args) { 6 File f = new File("/Users/nnngu/Downloads"); 7 for(File temp : f.listFiles()) { 8 if(temp.isFile()) { 9 System.out.println(temp.getName()); 10 } 11 } 12 } 13 }
如果需要对文件夹继续展开,代码如下所示:
1 import java.io.File; 2 3 class Test12 { 4 5 public static void main(String[] args) { 6 showDirectory(new File("/Users/nnngu/Downloads")); 7 } 8 9 public static void showDirectory(File f) { 10 _walkDirectory(f, 0); 11 } 12 13 private static void _walkDirectory(File f, int level) { 14 if(f.isDirectory()) { 15 for(File temp : f.listFiles()) { 16 _walkDirectory(temp, level + 1); 17 } 18 } 19 else { 20 for(int i = 0; i < level - 1; i++) { 21 System.out.print("\\t"); 22 } 23 System.out.println(f.getName()); 24 } 25 } 26 }
在Java 7中可以使用NIO.2的API来做同样的事情,代码如下所示:
1 class ShowFileTest { 2 3 public static void main(String[] args) throws IOException { 4 Path initPath = Paths.get("/Users/nnngu/Downloads"); 5 Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() { 6 7 @Override 8 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 9 throws IOException { 10 System.out.println(file.getFileName().toString()); 11 return FileVisitResult.CONTINUE; 12 } 13 14 }); 15 } 16 }
72、用Java的套接字编程实现一个多线程的回显(echo)服务器。
答:
1 import java.io.BufferedReader; 2 import java.io.IOException; 3 import java.io.InputStreamReader; 4 import java.io.PrintWriter; 5 import java.net.ServerSocket; 6 import java.net.Socket; 7 8 public class EchoServer { 9 10 private static final int ECHO_SERVER_PORT = 6789; 11 12 public static void main(String[] args) { 13 try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) { 14 System.out.println("服务器已经启动..."); 15 while(true) { 16 Socket client = server.accept(); 17 new Thread(new ClientHandler(client)).start(); 18 } 19 } catch (IOException e) { 20 e.printStackTrace(); 21 } 22 } 23 24 private static class ClientHandler implements Runnable { 25 private Socket client; 26 27 public ClientHandler(Socket client) { 28 this.client = client; 29 } 30 31 @Override 32 public void run() { 33 try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); 34 PrintWriter pw = new PrintWriter(client.getOutputStream())) { 35 String msg = br.readLine(); 36 System.out.println("收到" + client.getInetAddress() + "发送的: " + msg); 37 pw.println(msg); 38 pw.flush(); 39 } catch(Exception ex) { 40 ex.printStackTrace(); 41 } finally { 42 try { 43 client.close(); 44 } catch (IOException e) { 45 e.printStackTrace(); 46 } 47 } 48 } 49 } 50 51 }
注意:上面的代码使用了Java 7的TWR语法,由于很多外部资源类都间接的实现了AutoCloseable接口(单方法回调接口),因此可以利用TWR语法在try结束的时候通过回调的方式自动调用外部资源类的close()方法,避免书写冗长的finally代码块。此外,上面的代码用一个静态内部类实现线程的功能,使用多线程可以避免一个用户I/O操作所产生的中断影响其他用户对服务器的访问,简单的说就是一个用户的输入操作不会造成其他用户的阻塞。当然,上面的代码使用线程池可以获得更好的性能,因为频繁的创建和销毁线程所造成的开销也是不可忽视的。
下面是一段回显客户端测试代码:
1 import java.io.BufferedReader; 2 import java.io.InputStreamReader; 3 import java.io.PrintWriter; 4 import java.net.Socket; 5 import java.util.Scanner; 6 7 public class EchoClient { 8 9 public static void main(String[] args) throws Exception { 10 Socket client = new Socket("localhost", 6789); 11 Scanner sc = new Scanner(System.in); 12 System.out.print("请输入内容: "); 13 String msg = sc.nextLine(); 14 sc.close(); 15 PrintWriter pw = new PrintWriter(client.getOutputStream()); 16 pw.println(msg); 17 pw.flush(); 18 BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); 19 System.out.println(br.readLine()); 20 client.close(); 21 } 22 }
如果希望用NIO的多路复用套接字实现服务器,代码如下所示。NIO的操作虽然带来了更好的性能,但是有些操作是比较底层的,对于初学者来说还是有些难于理解。
1 import java.io.IOException; 2 import java.net.InetSocketAddress; 3 import java.nio.ByteBuffer; 4 import java.nio.CharBuffer; 5 import java.nio.channels.SelectionKey; 6 import java.nio.channels.Selector; 7 import java.nio.channels.ServerSocketChannel; 8 import java.nio.channels.SocketChannel; 9 import java.util.Iterator; 10 11 public class EchoServerNIO { 12 13 private static final int ECHO_SERVER_PORT = 6789; 14 private static final int ECHO_SERVER_TIMEOUT = 5000; 15 private static final int BUFFER_SIZE = 1024; 16 17 private static ServerSocketChannel serverChannel = null; 18 private static Selector selector = null; // 多路复用选择器 19 private static ByteBuffer buffer = null; // 缓冲区 20 21 public static void main(String[] args) { 22 init(); 23 listen(); 24 } 25 26 private static void init() { 27 try { 28 serverChannel = ServerSocketChannel.open(); 29 buffer = ByteBuffer.allocate(BUFFER_SIZE); 30 serverChannel.socket().bind(new InetSocketAddress(ECHO_SERVER_PORT)); 31 serverChannel.configureBlocking(false); 32 selector = Selector.open(); 33 serverChannel.register(selector, SelectionKey.OP_ACCEPT); 34 } catch (Exception e) { 35 throw new RuntimeException(e); 36 } 37 } 38 39 private static void listen() { 40 while (true) { 41 try { 42 if (selector.select(ECHO_SERVER_TIMEOUT) != 0) { 43 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); 44 while (it.hasNext()) { 45 SelectionKey key = it.next(); 46 it.remove(); 47 handleKey(key); 48 } 49 } 50 } catch (Exception e) { 51 e.printStackTrace(); 52 } 53 } 54 } 55 56 private static void handleKey(SelectionKey key) throws IOException { 57 SocketChannel channel = null; 58 59 try { 60 if (key.isAcceptable()) { 61 ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); 62 channel = serverChannel.accept(); 63 channel.configureBlocking(false); 64 channel.register(selector, SelectionKey.OP_READ); 65 } else if (key.isReadable()) { 66 channel = (SocketChannel) key.channel(); 67 buffer.clear(); 68 if (channel.read(buffer) > 0) { 69 buffer.flip(); 70 CharBuffer charBuffer = CharsetHelper.decode(buffer); 71 String msg = charBuffer.toString(); 72 System.out.println("收到" + channel.getRemoteAddress() + "的消息:" + msg); 73 channel.write(CharsetHelper.encode(CharBuffer.wrap(msg))); 74 } else { 75 channel.close(); 76 } 77 } 78 } catch (Exception e) { 79 e.printStackTrace(); 80 if (channel != null) { 81 channel.close(); 82 } 83 } 84 } 85 86 }
1 import java.nio.ByteBuffer; 2 import java.nio.CharBuffer; 3 import java.nio.charset.CharacterCodingException; 4 import java.nio.charset.Charset; 5 import java.nio.charset.CharsetDecoder; 6 import java.nio.charset.CharsetEncoder; 7 8 public final class CharsetHelper { 9 private static final String UTF_8 = "UTF-8"; 10 private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder(); 11 private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder(); 12 13 private CharsetHelper() { 14 } 15 16 public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{ 17 return encoder.encode(in); 18 } 19 20 public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{ 21 return decoder.decode(in); 22 } 23 }
73、XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式?
答:XML文档定义分为DTD和Schema两种形式,二者都是对XML语法的约束,其本质区别在于Schema本身也是一个XML文件,可以被XML解析器解析,而且可以为XML承载的数据定义类型,约束能力较之DTD更强大。对XML的解析主要有DOM(文档对象模型,Document Object Model)、SAX(Simple API for XML)和StAX(Java 6中引入的新的解析XML的方式,Streaming API for XML),其中DOM处理大型文件时其性能下降的非常厉害,这个问题是由DOM树结构占用的内存较多造成的,而且DOM解析方式必须在解析文件之前把整个文档装入内存,适合对XML的随机访问(典型的用空间换取时间的策略);SAX是事件驱动型的XML解析方式,它顺序读取XML文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过事件回调代码来处理XML文件,适合对XML的顺序访问;顾名思义,StAX把重点放在流上,实际上StAX与其他解析方式的本质区别就在于应用程序能够把XML作为一个事件流来处理。将XML作为一组事件来处理的想法并不新颖(SAX就是这样做的),但不同之处在于StAX允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。
74、你在项目中哪些地方用到了XML?
答:XML的主要作用有两个方面:数据交换和信息配置。在做数据交换时,XML将数据用标签组装成起来,然后压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再从XML文件中还原相关信息进行处理,XML曾经是异构系统间交换数据的事实标准,但此项功能几乎已经被JSON(javascript Object Notation)取而代之。当然,目前很多软件仍然使用XML来存储配置信息,我们在很多项目中通常也会将作为配置信息的硬代码写在XML文件中,Java的很多框架也是这么做的,而且这些框架都选择了dom4j作为处理XML的工具,因为Sun公司的官方API实在不怎么好用。
补充:现在有很多时髦的软件(如Sublime)已经开始将配置文件书写成JSON格式,我们已经强烈的感受到XML的另一项功能也将逐渐被业界抛弃。
75、阐述JDBC操作数据库的步骤。
答:下面的代码以连接本机的Oracle数据库为例,演示JDBC操作数据库的步骤。
加载驱动。
创建连接。
创建语句。
1 PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?"); 2 ps.setInt(1, 1000); 3 ps.setInt(2, 3000);
执行语句。
ResultSet rs = ps.executeQuery();
处理结果。
1 while(rs.next()) { 2 System.out.println(rs.getInt("empno") + " - " + rs.getString("ename")); 3 }
关闭资源。
1 finally { 2 if(con != null) { 3 try { 4 con.close(); 5 } catch (SQLException e) { 6 e.printStackTrace(); 7 } 8 } 9 }
提示:关闭外部资源的顺序应该和打开的顺序相反,也就是说先关闭ResultSet、再关闭Statement、在关闭Connection。上面的代码只关闭了Connection(连接),虽然通常情况下在关闭连接时,连接上创建的语句和打开的游标也会关闭,但不能保证总是如此,因此应该按照刚才说的顺序分别关闭。此外,第一步加载驱动在JDBC 4.0中是可以省略的(自动从类路径中加载驱动),但是我们建议保留。
76、Statement和PreparedStatement有什么区别?哪个性能更好?
答:与Statement相比,①PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL的编译错误并增加SQL的安全性(减少SQL注射攻击的可能性);②PreparedStatement中的SQL语句是可以带参数的,避免了用字符串连接拼接SQL语句的麻烦和不安全;③当批量处理SQL或频繁执行相同的查询时,PreparedStatement有明显的性能上的优势,由于数据库可以将编译优化后的SQL语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成执行计划)。
补充:为了提供对存储过程的调用,JDBC API中还提供了CallableStatement接口。存储过程(Stored Procedure)是数据库中一组为了完成特定功能的SQL语句的集合,经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。虽然调用存储过程会在网络开销、安全性、性能上获得很多好处,但是存在如果底层数据库发生迁移时就会有很多麻烦,因为每种数据库的存储过程在书写上存在不少的差别。
77、使用JDBC操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能?
答:要提升读取数据的性能,可以指定通过结果集(ResultSet)对象的setFetchSize()方法指定每次抓取的记录数(典型的空间换时间策略);要提升更新数据的性能可以使用PreparedStatement语句构建批处理,将若干SQL语句置于一个批处理中执行。
78、在进行数据库编程时,连接池有什么作用?
答:由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行TCP的三次握手,释放连接需要进行TCP四次握手,造成的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。池化技术在Java开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于Java的开源数据库连接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
补充:在计算机系统中时间和空间是不可调和的矛盾,理解这一点对设计满足性能要求的算法是至关重要的。大型网站性能优化的一个关键就是使用缓存,而缓存跟上面讲的连接池道理非常类似,也是使用空间换时间的策略。可以将热点数据置于缓存中,当用户查询这些数据时可以直接从缓存中得到,这无论如何也快过去数据库中查询。当然,缓存的置换策略等也会对系统性能产生重要影响,对于这个问题的讨论已经超出了这里要阐述的范围。
79、什么是DAO模式?
答:DAO(Data Access Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作。在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在一个公共API中。用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO模式实际上包含了两个模式,一是Data Accessor(数据访问器),二是Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。
80、事务的ACID是指什么?
答:
- 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
- 一致性(Consistent):事务结束后系统状态是一致的;
- 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
- 持久性(Durable):事务完成后所做的改动都会
以上是关于Java null最佳实践的主要内容,如果未能解决你的问题,请参考以下文章