互联网 Java 工程师面试题(Java 面试题三)

75、阐述 JDBC 操作数据库的步骤。

答: 下面的代码以连接本机的 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 中是可以省略的(自动从类路径中加载驱动),但是我们建议保留。

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):事务完成后所做的改动都会被持久化,即使发生灾难 性的失败。通过日志和同步备份可以在故障发生后重建数据。

补充:关于事务,在面试中被问到的概率是很高的,可以问的问题也是很多的。 首先需要知道的是,只有存在并发数据访问时才需要事务。当多个事务访问同一 数据时,可能会存在 5 类问题,包括 3 类数据读取问题(脏读、不可重复读和幻 读)和 2 类数据更新问题(第 1 类丢失更新和第 2 类丢失更新)。

脏读(Dirty Read):A 事务读取 B 事务尚未提交的数据并在此基础上操作,而 B 事务执行回滚,那么 A 读取到的数据就是脏数据。

互联网 Java 工程师面试题(Java 面试题三)_第1张图片

不可重复读(Unrepeatable Read):事务 A 重新读取前面读取过的数据,发现 该数据已经被另一个已提交的事务 B 修改过了。

互联网 Java 工程师面试题(Java 面试题三)_第2张图片

幻读(Phantom Read):事务 A 重新执行一个查询,返回一系列符合查询条件 的行,发现其中插入了被事务 B 提交的行。

互联网 Java 工程师面试题(Java 面试题三)_第3张图片

第 1 类丢失更新:事务 A 撤销时,把已经提交的事务 B 的更新数据覆盖了。

互联网 Java 工程师面试题(Java 面试题三)_第4张图片

第 2 类丢失更新:事务 A 覆盖事务 B 已经提交的数据,造成事务 B 所做的操作丢 失。

互联网 Java 工程师面试题(Java 面试题三)_第5张图片

数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能 就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,按锁定对象不 同可以分为表级锁和行级锁;按并发事务锁定关系可以分为共享锁和独占锁,具 体的内容大家可以自行查阅资料进行了解。 直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定 会话的事务隔离级别,数据库就会通过分析 SQL 语句然后为事务访问的资源加上 合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对 用户来说都是透明的(就是说你不用理解,事实上我确实也不知道)。ANSI/ISO SQL 92 标准定义了 4 个等级的事务隔离级别,如下表所示:

互联网 Java 工程师面试题(Java 面试题三)_第6张图片

需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高 并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方有万能的原则。

81、JDBC 中如何进行事务处理?

答: Connection 提供了事务处理的方法,通过调用 setAutoCommit(false)可以设置 手动提交事务;当事务完成后用 commit()显式提交事务;如果在事务处理过程中 发生异常则通过 rollback()进行事务回滚。除此之外,从 JDBC 3.0 中还引入了 Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保 存点。

互联网 Java 工程师面试题(Java 面试题三)_第7张图片

82、JDBC 能否处理 Blob 和 Clob?

答: 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}

83、简述正则表达式及其用途。

答: 在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。 正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本 规则的代码。

说明:计算机诞生初期处理的信息几乎都是数值,但是时过境迁,今天我们使计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符 串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。

84、Java 中是如何支持正则表达式操作的?

答: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 分钟入门教程》。

85、获得一个类的类对象有哪些方式?

答:

  • 方法 1:类型.class,例如:String.class
  • 方法 2:对象.getClass(),例如:”hello”.getClass()
  • 方法 3:Class.forName(),例如:Class.forName(“java.lang.String”)

86、如何通过反射创建对象?

答:

  • 方法 1:通过类对象调用 newInstance()方法,例如: String.class.newInstance()
  • 方法 2:通过类对象的 getConstructor()或 getDeclaredConstructor() 方法获得构造器(Constructor)对象并调用其 newInstance()方法创建对象,

例如:String.class.getConstructor(String.class).newInstance(“Hello”);

87、如何通过反射获取和设置对象私有字段的值?

答: 可以通过类对象的 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}

88、如何通过反射调用对象的方法?

答: 请看下面的代码:

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}

89、简述一下面向对象的”六原则一法则”。

答:

  • 单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是” 高内聚”,写代码最终极的原则只有六个字”高内聚、低耦合”,就如同葵花宝 典或辟邪剑谱的中心思想就八个字”欲练此功必先自宫”,所谓的高内聚就是一 个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事, 而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。我 们都知道一句话叫”因为专注,所以专业”,一个对象如果承担太多的职责,那 么注定它什么都做不好。这个世界上任何好的东西都有两个特征,一个是功能单 一,好的相机绝对不是电视购物里面卖的那种一个机器有一百多种功能的,它基 本上只能照相;另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速 器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定 是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模 块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
  • 开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下, 当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类 就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:①抽象是关 键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系 统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系 统将变得复杂而换乱,如果不清楚如何封装可变性,可以参考《设计模式精解》 一书中对桥梁模式的讲解的章节。)
  • 依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方 法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不 用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的氏替换原则。)里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的 描述,Barbara Liskov 女士的描述比这个要复杂得多,但简单的说就是能用父 类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如 果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对 代码进行重构。例如让猫继承狗,或者狗继承猫,又或者让正方形继承长方形都 是错误的继承关系,因为你很容易找到违反里氏替换原则的场景。需要注意的是: 子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更 多,把能力多的对象当成能力少的对象来用当然没有任何问题。)
  • 接口隔离原则:接口要小而专,绝不能大而全。(臃肿的接口是对接口的 污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高 度内聚的。例如,琴棋书画就应该分别设计为四个接口,而不应设计成一个接口 中的四个方法,因为如果设计成一个接口中的四个方法,那么这个接口很难用, 毕竟琴棋书画四样都精通的人还是少数,而如果设计成四个接口,会几项就实现 几个接口,这样的话每个接口被复用的可能性是很高的。Java 中的接口代表能 力、代表约定、代表角色,能否正确的使用接口一定是编程水平高低的重要标识。)
  • 合成聚合复用原则:优先使用聚合或合成关系复用代码。(通过继承来复 用代码是面向对象程序设计中被滥用得最多的东西,因为所有的教科书都无一例 外的对继承进行了鼓吹从而误导了初学者,类与类之间简单的说有三种关系, Is-A 关系、Has-A 关系、Use-A 关系,分别代表继承、关联和依赖。其中,关 联关系根据其关联的强度又可以进一步划分为关联、聚合和合成,但说白了都是 Has-A 关系,合成聚合复用原则想表达的是优先考虑 Has-A 关系而不是 Is-A 关 系复用代码,原因嘛可以自己从百度上找到一万个理由,需要说明的是,即使在 Java 的 API 中也有不少滥用继承的例子,例如 Properties 类继承了 Hashtable 类,Stack 类继承了 Vector 类,这些继承明显就是错误的,更好的做法是在 Properties 类中放置一个 Hashtable 类型的成员并且将其键和值都设置为字符 串来存储数据,而 Stack 类的设计也应该是在 Stack 类中放一个 Vector 对象来 存储数据。记住:任何时候都不要继承工具类,工具是可以拥有并可以使用的, 而不是拿来继承的。)
  • 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有 尽可能少的了解。(迪米特法则简单的说就是如何做到”低耦合”,门面模式和调停者模式就是对迪米特法则的践行。对于门面模式可以举一个简单的例子,你 去一家公司洽谈业务,你不需要了解这个公司内部是如何运作的,你甚至可以对 这个公司一无所知,去的时候只需要找到公司入口处的前台美女,告诉她们你要 做什么,她们会找到合适的人跟你接洽,前台的美女就是公司这个系统的门面。 再复杂的系统都可以为用户提供一个简单的门面,Java Web 开发中作为前端控 制器的 Servlet 或 Filter 不就是一个门面吗,浏览器对服务器的运作方式一无所 知,但是通过前端控制器就能够根据你的请求得到相应的服务。调停者模式也可 以举一个简单的例子来说明,例如一台计算机,CPU、内存、硬盘、显卡、声卡 各种设备需要相互配合才能很好的工作,但是如果这些东西都直接连接到一起, 计算机的布线将异常复杂,在这种情况下,主板作为一个调停者的身份出现,它 将各个设备连接在一起而不需要每个设备之间直接交换数据,这样就减小了系统 的耦合度和复杂度,如下图所示。迪米特法则用通俗的话来将就是不要和陌生人 打交道,如果真的需要,找一个自己的朋友,让他替你和陌生人打交道。)

互联网 Java 工程师面试题(Java 面试题三)_第8张图片

互联网 Java 工程师面试题(Java 面试题三)_第9张图片

90、简述一下你了解的设计模式。

答: 所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经 过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他 人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计 和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解 其设计思路。

在 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(责任链模式)。

面试被问到关于设计模式的知识时,可以拣最常用的作答,例如:

  • 工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公 共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同 的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而 不必考虑到底返回的是哪一个子类的实例。
  • 代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引 用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、 保护代理、Cache 代理、防火墙代理、同步化代理、智能引用代理
  • 适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使 原本因接口不匹配而无法在一起使用的类能够一起工作。
  • 模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式 实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不 同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。

除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式 (Collections 工具类和 I/O 系统中都使用装潢模式)等,反正基本原则就是拣 自己最熟悉的、用得最多的作答,以免言多必失。

91、用 Java 写一个单例类。

答:

  • 饿汉式单例
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 容器可以为普通的类创建单例,它是怎么做到的呢?

92、什么是 UML?

答:UML 是统一建模语言(Unified Modeling Language)的缩写,它发表于 1997 年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型 化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支 持。使用 UML 可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系 统的结构和行为。

93、UML 中有哪些常用的图?

答: UML 定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结 构,包括:用例图(use case diagram)、类图(class diagram)、时序图(sequence diagram)、协作图(collaboration diagram)、状态图(statechart diagram)、 活动图(activity diagram)、构件图(component diagram)、部署图(deployment diagram)等。在这些图形化符号中,有三种图最为重要,分别是:用例图(用来 捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系)、 类图(描述类以及类与类之间的关系,通过该图可以快速了解系统)、时序图(描 述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能 接收的消息也就是说对象能够向外界提供的服务)。 用例图:

互联网 Java 工程师面试题(Java 面试题三)_第10张图片

类图:

互联网 Java 工程师面试题(Java 面试题三)_第11张图片

时序图:

互联网 Java 工程师面试题(Java 面试题三)_第12张图片

94、用 Java 写一个冒泡排序。

答: 冒泡排序几乎是个程序员都写得出来,但是面试的时候如何写一个逼格高的冒泡 排序却不是每个人都能做到,下面提供一个参考代码:

 1import java.util.Comparator;
 2/**
 3* 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可
 4以相互替换)
 5* @author 骆昊
 6*
 7*/
 8public interface Sorter {
 9/**
10* 排序
11* @param list 待排序的数组
12*/
13public > void sort(T[] list);
14/**
15* 排序
16* @param list 待排序的数组
17* @param comp 比较两个对象的比较器
18*/
19public  void sort(T[] list, Comparator comp);
20}
 1import java.util.Comparator;
 2/**
 3* 冒泡排序
 4*
 5* @author 骆昊
 6*
 7*/
 8public class BubbleSorter implements Sorter {
 9@Override
10public > 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  void sort(T[] list, Comparator 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}

95、用 Java 写一个折半查找。

答: 折半查找,也称二分查找、二分搜索,是一种在有序数组中查找某一特定元素的 搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小 于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜 索范围缩小一半,其时间复杂度是 O(logN)。

 1import java.util.Comparator;
 2public class MyUtil {
 3public static > int binarySearch(T[] x, T
 4key) {
 5return binarySearch(x, 0, x.length- 1, key);
 6}
 7// 使用循环实现的二分查找
 8public static  int binarySearch(T[] x, T key, Comparator 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> 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(>>>是逻辑右移,是 不带符号位的右移)

你可能感兴趣的:(java,面试,工程师)