答: 下面的代码以连接本机的 Oracle 数据库为例,演示 JDBC 操作数据库的步骤。
1Class.forName("oracle.jdbc.driver.OracleDriver");
1Connection con = 2DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", 3"scott", "tiger");
1PreparedStatement ps = con.prepareStatement("select * from emp 2where sal between ? and ?"); 3ps.setInt(1, 1000); 4ps.setInt(2, 3000);
1ResultSet rs = ps.executeQuery();
1while(rs.next()) { 2System.out.println(rs.getInt("empno") + " - " + 3rs.getString("ename")); 4}
1finally { 2if(con != null) { 3try { 4con.close(); 5} catch (SQLException e) { 6e.printStackTrace(); 7} 8} 9}
提示:关闭外部资源的顺序应该和打开的顺序相反,也就是说先关闭 ResultSet、 再关闭 Statement、在关闭 Connection。上面的代码只关闭了 Connection(连 接),虽然通常情况下在关闭连接时,连接上创建的语句和打开的游标也会关闭, 但不能保证总是如此,因此应该按照刚才说的顺序分别关闭。此外,第一步加载 驱动在 JDBC 4.0 中是可以省略的(自动从类路径中加载驱动),但是我们建议保留。
答: 与 Statement 相比,①PreparedStatement 接口代表预编译的语句,它主要的优 势在于可以减少 SQL 的编译错误并增加 SQL 的安全性(减少 SQL 注射攻击的可 能性);②PreparedStatement 中的 SQL 语句是可以带参数的,避免了用字符串 连接拼接 SQL 语句的麻烦和不安全;③当批量处理 SQL 或频繁执行相同的查询时, PreparedStatement 有明显的性能上的优势,由于数据库可以将编译优化后的 SQL 语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成 执行计划)。
补充:为了提供对存储过程的调用,JDBC API 中还提供了 CallableStatement 接 口。存储过程(Stored Procedure)是数据库中一组为了完成特定功能的 SQL 语 句的集合,经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数 (如果该存储过程带有参数)来执行它。虽然调用存储过程会在网络开销、安全 性、性能上获得很多好处,但是存在如果底层数据库发生迁移时就会有很多麻烦, 因为每种数据库的存储过程在书写上存在不少的差别。
答: 要提升读取数据的性能,可以指定通过结果集(ResultSet)对象的 setFetchSize(方法指定每次抓取的记录数(典型的空间换时间策略);要提升更新数据的性能 可以使用 PreparedStatement 语句构建批处理,将若干 SQL 语句置于一个批处 理中执行。
答: 由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每 次建立连接都需要进行 TCP 的三次握手,释放连接需要进行 TCP 四次握手,造成 的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连 接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭 连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间 的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。池化技术在 Java 开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于 Java 的 开源数据库连接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid 等
补充:在计算机系统中时间和空间是不可调和的矛盾,理解这一点对设计满足性 能要求的算法是至关重要的。大型网站性能优化的一个关键就是使用缓存,而缓 存跟上面讲的连接池道理非常类似,也是使用空间换时间的策略。可以将热点数 据置于缓存中,当用户查询这些数据时可以直接从缓存中得到,这无论如何也快 过去数据库中查询。当然,缓存的置换策略等也会对系统性能产生重要影响,对 于这个问题的讨论已经超出了这里要阐述的范围。
答: DAO(Data Access Object)顾名思义是一个为数据库或其他持久化机制提供了 抽象接口的对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访 问操作。在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在 一个公共 API 中。用程序设计语言来说,就是建立一个接口,接口中定义了此应 用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该 类对应一个特定的数据存储。DAO 模式实际上包含了两个模式,一是 Data Accessor(数据访问器),二是 Data Object(数据对象),前者要解决如何访 问数据的问题,而后者要解决的是如何用对象封装数据。
答:
补充:关于事务,在面试中被问到的概率是很高的,可以问的问题也是很多的。 首先需要知道的是,只有存在并发数据访问时才需要事务。当多个事务访问同一 数据时,可能会存在 5 类问题,包括 3 类数据读取问题(脏读、不可重复读和幻 读)和 2 类数据更新问题(第 1 类丢失更新和第 2 类丢失更新)。
脏读(Dirty Read):A 事务读取 B 事务尚未提交的数据并在此基础上操作,而 B 事务执行回滚,那么 A 读取到的数据就是脏数据。
不可重复读(Unrepeatable Read):事务 A 重新读取前面读取过的数据,发现 该数据已经被另一个已提交的事务 B 修改过了。
幻读(Phantom Read):事务 A 重新执行一个查询,返回一系列符合查询条件 的行,发现其中插入了被事务 B 提交的行。
第 1 类丢失更新:事务 A 撤销时,把已经提交的事务 B 的更新数据覆盖了。
第 2 类丢失更新:事务 A 覆盖事务 B 已经提交的数据,造成事务 B 所做的操作丢 失。
数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能 就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,按锁定对象不 同可以分为表级锁和行级锁;按并发事务锁定关系可以分为共享锁和独占锁,具 体的内容大家可以自行查阅资料进行了解。 直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定 会话的事务隔离级别,数据库就会通过分析 SQL 语句然后为事务访问的资源加上 合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对 用户来说都是透明的(就是说你不用理解,事实上我确实也不知道)。ANSI/ISO SQL 92 标准定义了 4 个等级的事务隔离级别,如下表所示:
需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高 并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方有万能的原则。
答: Connection 提供了事务处理的方法,通过调用 setAutoCommit(false)可以设置 手动提交事务;当事务完成后用 commit()显式提交事务;如果在事务处理过程中 发生异常则通过 rollback()进行事务回滚。除此之外,从 JDBC 3.0 中还引入了 Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保 存点。
答: Blob 是指二进制大对象(Binary Large Object),而 Clob 是指大字符对象 (Character Large Objec),因此其中 Blob 是为存储大的二进制数据而设计的, 而 Clob 是为存储大的文本数据而设计的。JDBC 的 PreparedStatement 和 ResultSet 都提供了相应的方法来支持 Blob 和 Clob 操作。下面的代码展示了如 何使用 JDBC 操作 LOB:
下面以 MySQL 数据库为例,创建一个张有三个字段的用户表,包括编号(id)、 姓名(name)和照片(photo),建表语句如下:
1create table tb_user 2( 3id int primary key auto_increment, 4name varchar(20) unique not null, 5photo longblob 6)
下面的 Java 代码向数据库中插入一条记录:
1import java.io.FileInputStream; 2import java.io.IOException; 3import java.io.InputStream; 4import java.sql.Connection; 5import java.sql.DriverManager; 6import java.sql.PreparedStatement; 7import java.sql.SQLException; 8class JdbcLobTest { 9public static void main(String[] args) { 10Connection con = null; 11try { 12// 1. 加载驱动(Java6 以上版本可以省略) 13Class.forName("com.mysql.jdbc.Driver"); 14// 2. 建立连接 15con = 16DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", 17"123456"); 18// 3. 创建语句对象 19PreparedStatement ps = con.prepareStatement("insert into 20tb_user values (default, ?, ?)"); 21ps.setString(1, "骆昊"); // 将 SQL 语句中第一个 22占位符换成字符串 23try (InputStream in = new FileInputStream("test.jpg")) 24{ // Java 7 的 TWR 25ps.setBinaryStream(2, in); // 将 SQL 语句中第二个占 26位符换成二进制流 27// 4. 发出 SQL 语句获得受影响行数 28System.out.println(ps.executeUpdate() == 1 ? "插入成功 29" : "插入失败"); 30} catch(IOException e) { 31System.out.println("读取照片失败!"); 32} 33} catch (ClassNotFoundException | SQLException e) { // Java 347 的多异常捕获 35e.printStackTrace(); 36} finally { // 释放外部资源的代码都应当放在 finally 中保证其能够得 37到执行 38try { 39if(con != null && !con.isClosed()) { 40con.close(); // 5. 释放数据库连接 41con = null; // 指示垃圾回收器可以回收该对象 42} 43} catch (SQLException e) { 44e.printStackTrace(); 45} 46} 47} 48}
答: 在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。 正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本 规则的代码。
说明:计算机诞生初期处理的信息几乎都是数值,但是时过境迁,今天我们使计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符 串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。
答:Java 中的 String 类提供了支持正则表达式操作的方法,包括:matches()、 replaceAll()、replaceFirst()、split()。此外,Java 中可以用 Pattern 类表示正则 表达式对象,它提供了丰富的 API 进行各种正则表达式操作,请参考下面面试题 的代码。
面试题: - 如果要从字符串中截取第一个英文左括号之前的字符串,例如:北京 市(朝阳区)(西城区)(海淀区),截取结果为:北京市,那么正则表达式怎么写?
1import java.util.regex.Matcher; 2import java.util.regex.Pattern; 3class RegExpTest { 4public static void main(String[] args) { 5String str = "北京市(朝阳区)(西城区)(海淀区)"; 6Pattern p = Pattern.compile(".*?(?=//()"); 7Matcher m = p.matcher(str); 8if(m.find()) { 9System.out.println(m.group()); 10} 11} 12}
说明:上面的正则表达式中使用了懒惰匹配和前瞻,如果不清楚这些内容,推荐 读一下网上很有名的《正则表达式 30 分钟入门教程》。
答:
答:
例如:String.class.getConstructor(String.class).newInstance(“Hello”);
答: 可以通过类对象的 getDeclaredField()方法字段(Field)对象,然后再通过字段 对象的 setAccessible(true)将其设置为可以访问,接下来就可以通过 get/set 方 法来获取/设置字段的值了。下面的代码实现了一个反射的工具类,其中的两个静 态方法分别用于获取和设置私有字段的值,字段可以是基本类型也可以是对象类 型且支持多级对象操作,例如 ReflectionUtil.get(dog, “owner.car.engine.id”); 可以获得 dog 对象的主人的汽车的引擎的 ID 号。
1import java.lang.reflect.Constructor; 2import java.lang.reflect.Field; 3import java.lang.reflect.Modifier; 4import java.util.ArrayList; 5import java.util.List; 6/** 7* 反射工具类 8* @author 骆昊 9* 10*/ 11public class ReflectionUtil { 12private ReflectionUtil() { 13throw new AssertionError(); 14} 15/** 16* 通过反射取对象指定字段(属性)的值 17* @param target 目标对象 18* @param fieldName 字段的名字 19* @throws 如果取不到对象指定字段的值则抛出异常 20* @return 字段的值 21*/ 22public static Object getValue(Object target, String fieldName) { 23Class<?> clazz = target.getClass(); 24String[] fs = fieldName.split("//."); 25try { 26for(int i = 0; i < fs.length - 1; i++) { 27Field f = clazz.getDeclaredField(fs[i]); 28f.setAccessible(true); 29target = f.get(target); 30clazz = target.getClass(); 31} 32Field f = clazz.getDeclaredField(fs[fs.length - 1]); 33f.setAccessible(true); 34return f.get(target); 35} 36catch (Exception e) { 37throw new RuntimeException(e); 38} 39} 40/** 41* 通过反射给对象的指定字段赋值 42* @param target 目标对象 43* @param fieldName 字段的名称 44* @param value 值 45*/ 46public static void setValue(Object target, String fieldName, Object 47value) { 48Class<?> clazz = target.getClass(); 49String[] fs = fieldName.split("//."); 50try { 51for(int i = 0; i < fs.length - 1; i++) { 52Field f = clazz.getDeclaredField(fs[i]); 53f.setAccessible(true); 54Object val = f.get(target); 55if(val == null) { 56Constructor<?> c = 57f.getType().getDeclaredConstructor(); 58c.setAccessible(true); 59val = c.newInstance(); 60f.set(target, val); 61} 62target = val; 63clazz = target.getClass(); 64} 65Field f = clazz.getDeclaredField(fs[fs.length - 1]); 66f.setAccessible(true); 67f.set(target, value); 68} 69catch (Exception e) { 70throw new RuntimeException(e); 71} 72} 73}
答: 请看下面的代码:
1import java.lang.reflect.Method; 2class MethodInvokeTest { 3public static void main(String[] args) throws Exception { 4String str = "hello"; 5Method m = str.getClass().getMethod("toUpperCase"); 6System.out.println(m.invoke(str)); // HELLO 7} 8}
答:
答: 所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经 过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他 人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计 和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解 其设计思路。
在 GoF 的《Design Patterns: Elements of Reusable Object-OrientedSoftware》中给出了三类(创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化])共 23 种设计模式,包括:Abstract Factory(抽象工厂模式),Builder(建造者模式),Factory Method(工厂方法模式),Prototype(原始模型模式),Singleton(单例模式);Facade(门面模式),Adapter(适配器模式),Bridge(桥梁模式),Composite(合成模式),Decorator(装饰模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(解释器模式),Visitor(访问者模式),Iterator(迭代子模式), Mediator(调停者模式),Memento(备忘录模式),Observer(观察者模式), State(状态模式),Strategy(策略模式),Template Method(模板方法模式), Chain Of Responsibility(责任链模式)。
面试被问到关于设计模式的知识时,可以拣最常用的作答,例如:
除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式 (Collections 工具类和 I/O 系统中都使用装潢模式)等,反正基本原则就是拣 自己最熟悉的、用得最多的作答,以免言多必失。
答:
1public class Singleton { 2private Singleton(){} 3private static Singleton instance = new Singleton(); 4public static Singleton getInstance(){ 5return instance; 6} 7}
1public class Singleton { 2private static Singleton instance = null; 3private Singleton() {} 4public static synchronized Singleton getInstance(){ 5if (instance == null) instance = new Singleton(); 6return instance; 7} 8}
注意:实现一个单例有两点注意事项,①将构造器私有,不允许外界通过构造器创建对象;②通过公开的静态方法向外界返回类的唯一实例。这里有一个问题可以思考:Spring 的 IoC 容器可以为普通的类创建单例,它是怎么做到的呢?
答:UML 是统一建模语言(Unified Modeling Language)的缩写,它发表于 1997 年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型 化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支 持。使用 UML 可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系 统的结构和行为。
答: UML 定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结 构,包括:用例图(use case diagram)、类图(class diagram)、时序图(sequence diagram)、协作图(collaboration diagram)、状态图(statechart diagram)、 活动图(activity diagram)、构件图(component diagram)、部署图(deployment diagram)等。在这些图形化符号中,有三种图最为重要,分别是:用例图(用来 捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系)、 类图(描述类以及类与类之间的关系,通过该图可以快速了解系统)、时序图(描 述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能 接收的消息也就是说对象能够向外界提供的服务)。 用例图:
类图:
时序图:
答: 冒泡排序几乎是个程序员都写得出来,但是面试的时候如何写一个逼格高的冒泡 排序却不是每个人都能做到,下面提供一个参考代码:
1import java.util.Comparator; 2/** 3* 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可 4以相互替换) 5* @author 骆昊 6* 7*/ 8public interface Sorter { 9/** 10* 排序 11* @param list 待排序的数组 12*/ 13public <T extends Comparable<T>> void sort(T[] list); 14/** 15* 排序 16* @param list 待排序的数组 17* @param comp 比较两个对象的比较器 18*/ 19public <T> void sort(T[] list, Comparator<T> comp); 20}
1import java.util.Comparator; 2/** 3* 冒泡排序 4* 5* @author 骆昊 6* 7*/ 8public class BubbleSorter implements Sorter { 9@Override 10public <T extends Comparable<T>> void sort(T[] list) { 11boolean swapped = true; 12for (int i = 1, len = list.length; i < len && swapped; ++i) { 13swapped = false; 14for (int j = 0; j < len - i; ++j) { 15if (list[j].compareTo(list[j + 1]) > 0) { 16T temp = list[j]; 17list[j] = list[j + 1]; 18list[j + 1] = temp; 19swapped = true; 20} 21} 22} 23} 24@Override 25public <T> void sort(T[] list, Comparator<T> comp) { 26boolean swapped = true; 27for (int i = 1, len = list.length; i < len && swapped; ++i) { 28swapped = false; 29for (int j = 0; j < len - i; ++j) { 30if (comp.compare(list[j], list[j + 1]) > 0) { 31T temp = list[j]; 32list[j] = list[j + 1]; 33list[j + 1] = temp; 34swapped = true; 35} 36} 37} 38} 39}
答: 折半查找,也称二分查找、二分搜索,是一种在有序数组中查找某一特定元素的 搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小 于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜 索范围缩小一半,其时间复杂度是 O(logN)。
1import java.util.Comparator; 2public class MyUtil { 3public static <T extends Comparable<T>> int binarySearch(T[] x, T 4key) { 5return binarySearch(x, 0, x.length- 1, key); 6} 7// 使用循环实现的二分查找 8public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) 9{ 10int low = 0; 11int high = x.length - 1; 12while (low <= high) { 13int mid = (low + high) >>> 1; 14int cmp = comp.compare(x[mid], key); 15if (cmp < 0) { 16low= mid + 1; 17} 18else if (cmp > 0) { 19high= mid - 1; 20} 21else { 22return mid; 23} 24} 25return -1; 26} 27// 使用递归实现的二分查找 28private static<T extends Comparable<T>> int binarySearch(T[] x, int 29low, int high, T key) { 30if(low <= high) { 31int mid = low + ((high -low) >> 1); 32if(key.compareTo(x[mid])== 0) { 33return mid; 34} 35else if(key.compareTo(x[mid])< 0) { 36return binarySearch(x,low, mid - 1, key); 37} 38else { 39return binarySearch(x,mid + 1, high, key); 40} 41} 42return -1; 43} 44}
说明:上面的代码中给出了折半查找的两个版本,一个用递归实现,一个用循环 实现。需要注意的是计算中间位置时不应该使用(high+ low) / 2 的方式,因为加 法运算可能导致整数越界,这里应该使用以下三种方式之一:low + (high - low) / 2 或 low + (high – low) >> 1 或(low + high) >>> 1(>>>是逻辑右移,是 不带符号位的右移)