目录
82、JDBC 能否处理 Blob 和 Clob?
83、简述正则表达式及其用途。
84、Java 中是如何支持正则表达式操作的?
85、获得一个类的类对象有哪些方式?
86、如何通过反射创建对象?
87、如何通过反射获取和设置对象私有字段的值?
88、如何通过反射调用对象的方法?
89、简述一下面向对象的”六原则一法则”。
Blob 是指二进制大对象(Binary Large Object),而 Clob 是指大字符对象(Character Large Objec),因此其中 Blob 是为存储大的二进制数据而设计的,而 Clob 是为存储大的文本数据而设计的。JDBC 的 PreparedStatement 和ResultSet 都提供了相应的方法来支持 Blob 和 Clob 操作。下面的代码展示了如何使用 JDBC 操作 LOB:
下面以 MySQL 数据库为例,创建一个张有三个字段的用户表,包括编号(id)、姓名(name)和照片(photo),建表语句如下:
create table tb_user
(
id int primary key auto_increment,
name varchar(20) unique not null,
photo longblob
);
下面的 Java 代码向数据库中插入一条记录:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
class JdbcLobTest {
public static void main(String[] args) {
Connection con = null;
try {
// 1. 加载驱动(Java6 以上版本可以省略)
Class.forName("com.mysql.jdbc.Driver");
// 2. 建立连接
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
// 3. 创建语句对象
PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)");
ps.setString(1, "骆昊");// 将 SQL 语句中第一个占位符换成字符串
try (InputStream in = new FileInputStream("test.jpg"))
{
// Java 7 的 TWR
ps.setBinaryStream(2, in);
// 将 SQL 语句中第二个占位符换成二进制流
// 4. 发出 SQL 语句获得受影响行数
System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失败");
} catch(IOException e) {
System.out.println("读取照片失败!");
}
} catch (ClassNotFoundException | SQLException e) { // Java7 的多异常捕获
e.printStackTrace();
} finally { // 释放外部资源的代码都应当放在 finally 中保证其能够得到执行
try {
if(con != null && !con.isClosed()) {
con.close();// 5. 释放数据库连接
con = null; // 指示垃圾回收器可以回收该对象
}
}
catch (SQLException e) {
e.printStackTrace();
}
}
}
}
在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。
说明:计算机诞生初期处理的信息几乎都是数值,但是时过境迁,今天我们使用计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。
Java 中的 String 类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java 中可以用 Pattern 类表示正则表达式对象,它提供了丰富的 API 进行各种正则表达式操作,请参考下面面试题的代码。
面试题: - 如果要从字符串中截取第一个英文左括号之前的字符串,例如:北京市(朝阳区)(西城区)(海淀区),截取结果为:北京市,那么正则表达式怎么写?
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class RegExpTest {
public static void main(String[] args) {
String str = "北京市(朝阳区)(西城区)(海淀区)";
Pattern p = Pattern.compile(".*?(?=\\()");
Matcher m = p.matcher(str);
if(m.find()) {
System.out.println(m.group());
}
}
}
说明:上面的正则表达式中使用了懒惰匹配和前瞻,如果不清楚这些内容,推荐读一下网上很有名的《正则表达式 30 分钟入门教程》。
方法 1:类型.class,例如:String.class
方法 2:对象.getClass(),例如:”hello”.getClass()
方法 3:Class.forName(),例如:Class.forName(“java.lang.String”)
方法 1:通过类对象调用 newInstance()方法,例如:String.class.newInstance()
方法 2:通过类对象的 getConstructor()或 getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其 newInstance()方法创建对象,
例如:String.class.getConstructor(String.class).newInstance(“Hello”);
可以通过类对象的 getDeclaredField()方法字段(Field)对象,然后再通过字段对象的 setAccessible(true)将其设置为可以访问,接下来就可以通过 get/set 方法来获取/设置字段的值了。下面的代码实现了一个反射的工具类,其中的两个静态方法分别用于获取和设置私有字段的值,字段可以是基本类型也可以是对象类型且支持多级对象操作,例如 ReflectionUtil.get(dog, “owner.car.engine.id”);可以获得 dog 对象的主人的汽车的引擎的 ID 号。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
public class ReflectionUtil {
private ReflectionUtil() {
throw new AssertionError();
}
/**
* 通过反射取对象指定字段(属性)的值
* @param target 目标对象
* @param fieldName 字段的名字
* @throws 如果取不到对象指定字段的值则抛出异常
* @return 字段的值
*/
public static Object getValue(Object target, String fieldName) {
Class> clazz = target.getClass();
String[] fs = fieldName.split("\\.");
try {
for(int i = 0; i < fs.length - 1; i++) {
Field f = clazz.getDeclaredField(fs[i]);
f.setAccessible(true);
target = f.get(target);
clazz = target.getClass();
}
Field f = clazz.getDeclaredField(fs[fs.length - 1]);
f.setAccessible(true);
return f.get(target);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 通过反射给对象的指定字段赋值
* @param target 目标对象
* @param fieldName 字段的名称
* @param value 值
*/
public static void setValue(Object target, String fieldName, Object value) {
Class> clazz = target.getClass();
String[] fs = fieldName.split("\\.");
try {
for(int i = 0; i < fs.length - 1; i++) {
Field f = clazz.getDeclaredField(fs[i]);
f.setAccessible(true);
Object val = f.get(target);
if(val == null) {
Constructor> c =
f.getType().getDeclaredConstructor();
c.setAccessible(true);
val = c.newInstance();
f.set(target, val);
}
target = val;
clazz = target.getClass();
}
Field f = clazz.getDeclaredField(fs[fs.length - 1]);
f.setAccessible(true);
f.set(target, value);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
import java.lang.reflect.Method;
class MethodInvokeTest {
public static void main(String[] args) throws Exception {
String str = "hello";
Method m = str.getClass().getMethod("toUpperCase");
System.out.println(m.invoke(str)); // HELLO
}
}
单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是”高内聚”,写代码最终极的原则只有六个字”高内聚、低耦合”,就如同葵花宝典或辟邪剑谱的中心思想就八个字”欲练此功必先自宫”,所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。我们都知道一句话叫”因为专注,所以专业”,一个对象如果承担太多的职责,那么注定它什么都做不好。这个世界上任何好的东西都有两个特征,一个是功能单一,好的相机绝对不是电视购物里面卖的那种一个机器有一百多种功能的,它基本上只能照相;另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:
①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;
②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱,如果不清楚如何封装可变性,可以参考《设计模式精解》一书中对桥梁模式的讲解的章节。)
依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的里氏替换原则。)
里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的描述,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面试宝典_时光の尘的博客-CSDN博客