下面的内容是对网上原有的Java面试题集及答案进行了全面修订之后给出的负责任的题目和答案,原来的题目中有很多重复题目和无价值的题目,还有不少的参考答案也是错误的,修改后的Java面试题集参照了JDK最新版本,去掉了EJB 2.x等无用内容,补充了数据结构和算法相关的题目、经典面试编程题、大型网站技术架构、操作系统、数据库、软件测试、设计模式、UML等内容,同时还对很多知识点进行了深入的剖析,例如hashCode方法的设计、垃圾收集的堆和代、Java新的并发编程、NIO.2等,相信对准备入职的Java程序员一定有所裨益。
Java程序员面试题集(1-50)
一、Java基础部分
1、面向对象的特征有哪些方面?
答:面向对象的特征主要有以下几个方面:
1)抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
2)继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(如果不能理解请阅读阎宏博士的《Java与模式》或《设计模式精解》中关于桥梁模式的部分)。
3)封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。
4)多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当A系统访问B系统提供的服务时,B系统有多种提供服务的方式,但一切对A系统来说都是透明的(就像电动剃须刀是A系统,它的供电系统是B系统,B系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A系统只会通过B类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1. 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2. 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
2、访问修饰符public,private,protected,以及不写(默认)时的区别?
答:区别如下:
作用域 当前类 同包 子类 其他
public √ √ √ √
protected √ √ √ ×
default √ √ × ×
private √ × × ×
类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。
3、String 是最基本的数据类型吗?
答:不是。Java中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type)和枚举类型(enumeration type),剩下的都是引用类型(reference type)。
4、float f=3.4;是否正确?
答:不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。
5、short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
答:对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋值给short型。而short s1 = 1; s1 += 1;可以正确编译,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
6、Java 有没有goto?
答:goto 是Java中的保留字,在目前版本的Java中没有使用。(根据James Gosling(Java之父)编写的《The Java Programming Language》一书的附录中给出了一个Java关键字列表,其中有goto和const,但是这两个是目前无法使用的关键字,因此有些地方将其称之为保留字,其实保留字这个词应该有更广泛的意义,因为熟悉C语言的程序员都知道,在系统类库中使用过的有特殊意义的单词或单词的组合都被视为保留字)
7、int 和Integer 有什么区别?
答:Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入不是对象的基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从JDK 1.5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
- package com.lovo;
-
- public class AutoUnboxingTest {
-
- public static void main(String[] args) {
- Integer a = new Integer(3);
- Integer b = 3;
- int c = 3;
- System.out.println(a == b);
- System.out.println(a == c);
- }
- }
补充:最近还遇到一个面试题,也是和自动装箱和拆箱相关的,代码如下所示:
- public class Test03 {
-
- public static void main(String[] args) {
- Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
-
- System.out.println(f1 == f2);
- System.out.println(f3 == f4);
- }
- }
如果不明就里很容易认为两个输出要么都是true要么都是false。首先需要注意的是f1、f2、f3、f4四个变量都是Integer对象,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。
- public static Integer valueOf(int i) {
- if (i >= IntegerCache.low && i <= IntegerCache.high)
- return IntegerCache.cache[i + (-IntegerCache.low)];
- return new Integer(i);
- }
IntegerCache是Integer的内部类,其代码如下所示:
-
-
-
-
-
-
-
-
-
-
-
- private static class IntegerCache {
- static final int low = -128;
- static final int high;
- static final Integer cache[];
-
- static {
-
- int h = 127;
- String integerCacheHighPropValue =
- sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
- if (integerCacheHighPropValue != null) {
- try {
- int i = parseInt(integerCacheHighPropValue);
- i = Math.max(i, 127);
-
- h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
- } catch( NumberFormatException nfe) {
-
- }
- }
- high = h;
-
- cache = new Integer[(high - low) + 1];
- int j = low;
- for(int k = 0; k < cache.length; k++)
- cache[k] = new Integer(j++);
-
-
- assert IntegerCache.high >= 127;
- }
-
- private IntegerCache() {}
- }
简单的说,如果字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2的结果是true,而f3==f4的结果是false。越是貌似简单的面试题其中的玄机就越多,需要面试者有相当深厚的功力。
8、&和&&的区别?
答:&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(“”),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
补充:如果你熟悉JavaScript,那你可能更能感受到短路运算的强大,想成为JavaScript的高手就先从玩转短路运算开始吧。
9、解释内存中的栈(stack)、堆(heap)和静态存储区的用法。
答:通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;而通过new关键字和构造器创建的对象放在堆空间;程序中的字面量(literal)如直接书写的100、“hello”和常量都是放在静态存储区中。栈空间操作最快但是也很小,通常大量的对象都是放在堆空间,整个内存包括硬盘上的虚拟内存都可以被当成堆空间来使用。
String str = new String(“hello”);
上面的语句中str放在栈上,用new创建出来的字符串对象放在堆上,而“hello”这个字面量放在静态存储区。
补充:较新版本的Java中使用了一项叫“逃逸分析“的技术,可以将一些局部对象放在栈上以提升对象的操作性能。
10、Math.round(11.5) 等于多少? Math.round(-11.5)等于多少?
答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。
11、swtich 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
答:早期的JDK中,switch(expr)中,expr可以是byte、short、char、int。从1.5版开始,Java中引入了枚举类型(enum),expr也可以是枚举,从JDK 1.7版开始,还可以是字符串(String)。长整型(long)是不可以的。
12、用最有效率的方法计算2乘以8?
答: 2 << 3(左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方)。
补充:我们为编写的类重写hashCode方法时,可能会看到如下所示的代码,其实我们不太理解为什么要使用这样的乘法运算来产生哈希码(散列码),而且为什么这个数是个素数,为什么通常选择31这个数?前两个问题的答案你可以自己百度一下,选择31是因为可以用移位和减法运算来代替乘法,从而得到更好的性能。说到这里你可能已经想到了:31 * num <==> (num << 5) - num,左移5位相当于乘以2的5次方(32)再减去自身就相当于乘以31。现在的VM都能自动完成这个优化。
- package com.loonstudio;
-
- public class PhoneNumber {
- private int areaCode;
- private String prefix;
- private String lineNumber;
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + areaCode;
- result = prime * result
- + ((lineNumber == null) ? 0 : lineNumber.hashCode());
- result = prime * result + ((prefix == null) ? 0 : prefix.hashCode());
- return result;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (getClass() != obj.getClass())
- return false;
- PhoneNumber other = (PhoneNumber) obj;
- if (areaCode != other.areaCode)
- return false;
- if (lineNumber == null) {
- if (other.lineNumber != null)
- return false;
- } else if (!lineNumber.equals(other.lineNumber))
- return false;
- if (prefix == null) {
- if (other.prefix != null)
- return false;
- } else if (!prefix.equals(other.prefix))
- return false;
- return true;
- }
-
- }
13、数组有没有length()方法?String 有没有length()方法?
答:数组没有length()方法,有length 的属性。String 有length()方法。JavaScript中,获得字符串的长度是通过length属性得到的,这一点容易和Java混淆。
14、在Java 中,如何跳出当前的多重嵌套循环?
答:在最外层循环前加一个标记如A,然后用break A;可以跳出多重循环。(Java中支持带标签的break和continue语句,作用有点类似于C和C++中的goto语句,但是就像要避免使用goto一样,应该避免使用带标签的break和continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好)
15、构造器(constructor)是否可被重写(override)?
答:构造器不能被继承,因此不能被重写,但可以被重载。
16、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?
答:不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。Java对于eqauls方法和hashCode方法是这样规定的:(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;(2)如果两个对象的hashCode相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
补充:关于equals和hashCode方法,很多Java程序都知道,但很多人也就是仅仅知道而已,在Joshua Bloch的大作《Effective Java》(很多软件公司,《Effective Java》、《Java编程思想》以及《重构:改善既有代码质量》是Java程序员必看书籍,如果你还没看过,那就赶紧去亚马逊买一本吧)中是这样介绍equals方法的:首先equals方法必须满足自反性(x.equals(x)必须返回true)、对称性(x.equals(y)返回true时,y.equals(x)也必须返回true)、传递性(x.equals(y)和y.equals(z)都返回true时,x.equals(z)也必须返回true)和一致性(当x和y引用的对象信息没有被修改时,多次调用x.equals(y)应该得到同样的返回值),而且对于任何非null值的引用x,x.equals(null)必须返回false。实现高质量的equals方法的诀窍包括:1. 使用==操作符检查“参数是否为这个对象的引用”;2. 使用instanceof操作符检查“参数是否为正确的类型”;3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;4. 编写完equals方法后,问自己它是否满足对称性、传递性、一致性;5. 重写equals时总是要重写hashCode;6. 不要将equals方法参数中的Object对象替换为其他的类型,在重写时不要忘掉@Override注解。
17、是否可以继承String 类?
答:String 类是final类,不可以被继承。
补充:继承String本身就是一个错误的行为,对String类型最好的重用方式是关联(HAS-A)而不是继承(IS-A)。
18、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
答:是值传递。Java 编程语言只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对象的引用是永远不会改变的。C++和C#中可以通过传引用或传输出参数来改变传入的参数的值。
补充:Java中没有传引用实在是非常的不方便,这一点在Java 8中仍然没有得到改进,正是如此在Java编写的代码中才会出现大量的Wrapper类(将需要通过方法调用修改的引用置于一个Wrapper类中,再将Wrapper对象传入方法),这样的做法只会让代码变得臃肿,尤其是让从C和C++转型为Java程序员的开发者无法容忍。
19、String 和StringBuilder、StringBuffer 的区别?
答:Java 平台提供了两种类型的字符串:String和StringBuffer / StringBuilder,它们可以储存和操作字符串。其中String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。而StringBuffer和StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder是JDK 1.5中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer略高。
补充1:有一个面试题问:有没有哪种情况用+做字符串连接比调用StringBuffer / StringBuilder对象的append方法性能更好?如果连接后得到的字符串在静态存储区中是早已存在的,那么用+做字符串连接是优于StringBuffer / StringBuilder的append方法的。
补充2:下面也是一个面试题,问程序的输出,看看自己能不能说出正确答案。
- package com.lovo;
-
- public class StringEqualTest {
-
- public static void main(String[] args) {
- String a = "Programming";
- String b = new String("Programming");
- String c = "Program" + "ming";
-
- System.out.println(a == b);
- System.out.println(a == c);
- System.out.println(a.equals(b));
- System.out.println(a.equals(c));
- System.out.println(a.intern() == b.intern());
- }
- }
20、重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。
补充:华为的面试题中曾经问过这样一个问题:为什么不能根据返回类型来区分重载,说出你的答案吧!
21、描述一下JVM 加载class文件的原理机制?
答:JVM 中类的装载是由类加载器(ClassLoader) 和它的子类来实现的,Java中的类加载器是一个重要的Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。
补充:
1.由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2如果类中存在初始化语句,就依次执行这些初始化语句。
2.类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从JDK 1.2开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
a)Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
b)Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
c)System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
22、char 型变量中能不能存贮一个中文汉字?为什么?
答:char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char类型占2个字节(16bit),所以放一个中文是没问题的。
补充:使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于C程序员来说,要完成这样的编码转换恐怕要依赖于union(联合体/共用体)共享内存的特征来实现了。
23、抽象类(abstract class)和接口(interface)有什么异同?
答:抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
24、静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?
答:Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化,其语法看起来挺诡异的,如下所示。
- package com.lovo;
-
-
-
-
-
-
- public class Poker {
- private static String[] suites = {"黑桃", "红桃", "草花", "方块"};
- private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
-
- private Card[] cards;
-
-
-
-
-
- public Poker() {
- cards = new Card[52];
- for(int i = 0; i < suites.length; i++) {
- for(int j = 0; j < faces.length; j++) {
- cards[i * 13 + j] = new Card(suites[i], faces[j]);
- }
- }
- }
-
-
-
-
-
- public void shuffle() {
- for(int i = 0, len = cards.length; i < len; i++) {
- int index = (int) (Math.random() * len);
- Card temp = cards[index];
- cards[index] = cards[i];
- cards[i] = temp;
- }
- }
-
-
-
-
-
-
- public Card deal(int index) {
- return cards[index];
- }
-
-
-
-
-
-
-
- public class Card {
- private String suite;
- private int face;
-
- public Card(String suite, int face) {
- this.suite = suite;
- this.face = face;
- }
-
- @Override
- public String toString() {
- String faceStr = "";
- switch(face) {
- case 1: faceStr = "A"; break;
- case 11: faceStr = "J"; break;
- case 12: faceStr = "Q"; break;
- case 13: faceStr = "K"; break;
- default: faceStr = String.valueOf(face);
- }
- return suite + faceStr;
- }
- }
- }
测试类:
- package com.lovo;
-
- class PokerTest {
-
- public static void main(String[] args) {
- Poker poker = new Poker();
- poker.shuffle();
- Poker.Card c1 = poker.deal(0);
-
-
- Poker.Card c2 = poker.new Card("红心", 1);
-
- System.out.println(c1);
- System.out.println(c2);
- }
- }
25、Java 中会存在内存泄漏吗,请简单描述。
答:理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收也会发生内存泄露。一个例子就是hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象。下面的例子也展示了Java中发生内存泄露的情况:
- package com.lovo;
-
- import java.util.Arrays;
- import java.util.EmptyStackException;
-
- public class MyStack<T> {
- private T[] elements;
- private int size = 0;
-
- private static final int INIT_CAPACITY = 16;
-
- public MyStack() {
- elements = (T[]) new Object[INIT_CAPACITY];
- }
-
- public void push(T elem) {
- ensureCapacity();
- elements[size++] = elem;
- }
-
- public T pop() {
- if(size == 0)
- throw new EmptyStackException();
- return elements[--size];
- }
-
- private void ensureCapacity() {
- if(elements.length == size) {
- elements = Arrays.copyOf(elements, 2 * size + 1);
- }
- }
- }
上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的pop方法却存在内存泄露的问题,当我们用pop方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发Disk Paging(物理内存与硬盘的虚拟内存交换数据),甚至造成OutOfMemoryError。
26、抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被synchronized修饰?
答:都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。
27、静态变量和实例变量的区别?
答:静态变量是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。在Java开发中,上下文类和工具类中通常会有大量的静态成员。
28、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
答:不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,因此在调用静态方法时可能对象并没有被初始化。
29、如何实现对象克隆?
答:有两种方式:
1.实现Cloneable接口并重写Object类中的clone()方法;
2.实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。
- package com.lovo;
-
- import java.io.ByteArrayInputStream;
- import java.io.ByteArrayOutputStream;
- import java.io.ObjectInputStream;
- import java.io.ObjectOutputStream;
-
- public class MyUtil {
-
- private MyUtil() {
- throw new AssertionError();
- }
-
- public static <T> T clone(T obj) throws Exception {
- ByteArrayOutputStream bout = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(bout);
- oos.writeObject(obj);
-
- ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
- ObjectInputStream ois = new ObjectInputStream(bin);
- return (T) ois.readObject();
-
-
-
- }
- }
下面是测试代码:
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。
30、GC 是什么?为什么要有GC?
答:GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。
补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:
- 伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。
- 幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
- 终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。
与垃圾回收相关的JVM参数:
- -Xms / -Xmx --- 堆的初始大小 / 堆的最大大小
- -Xmn --- 堆中年轻代的大小
- -XX:-DisableExplicitGC --- 让System.gc()不产生任何作用
- -XX:+PrintGCDetail --- 打印GC的细节
- -XX:+PrintGCDateStamps --- 打印GC操作的时间戳
31、String s=new String(“xyz”);创建了几个字符串对象?
答:两个对象,一个是静态存储区的"xyz",一个是用new创建在堆上的对象。
32、接口是否可继承(extends)接口? 抽象类是否可实现(implements)接口? 抽象类是否可继承具体类(concrete class)?
答:接口可以继承接口。抽象类可以实现(implements)接口,抽象类可继承具体类,但前提是具体类必须有明确的构造函数。
33、一个“.java”源文件中是否可以包含多个类(不是内部类)?有什么限制?
答:可以,但一个源文件中最多只能有一个公开类(public class)而且文件名必须和公开类的类名完全保持一致。
34、Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
答:可以继承其他类或实现其他接口,在Swing编程中常用此方式来实现事件监听和回调。
35、内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
答:一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。
36、Java 中的final关键字有哪些用法?
答:(1)修饰类:表示该类不能被继承;(2)修饰方法:表示方法不能被重写;(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。
37、指出下面程序的运行结果:
- class A{
-
- static{
- System.out.print("1");
- }
-
- public A(){
- System.out.print("2");
- }
- }
-
- class B extends A{
-
- static{
- System.out.print("a");
- }
-
- public B(){
- System.out.print("b");
- }
- }
-
- public class Hello{
-
- public static void main(String[] args){
- A ab = new B();
- ab = new B();
- }
-
- }
答:执行结果:1a2b2b。创建对象时构造器的调用顺序是:先初始化静态成员,然后调用父类构造器,再初始化非静态成员,最后调用自身构造器。
38、数据类型之间的转换:
1)如何将字符串转换为基本数据类型?
2)如何将基本数据类型转换为字符串?
答:
1)调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)即可返回相应基本类型;
2)一种方法是将基本数据类型与空字符串(””)连接(+)即可获得其所对应的字符串;另一种方法是调用String 类中的valueOf(…)方法返回相应字符串
39、如何实现字符串的反转及替换?
答:方法很多,可以自己写实现也可以使用String或StringBuffer / StringBuilder中的方法。有一道很常见的面试题是用递归实现字符串反转,代码如下所示:
- public static String reverse(String originStr) {
- if(originStr == null || originStr.length() <= 1)
- return originStr;
- return reverse(originStr.substring(1)) + originStr.charAt(0);
- }
40、怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串?
答:代码如下所示:
String s1 = "你好";
String s2 = newString(s1.getBytes("GB2312"), "ISO-8859-1");
41、日期和时间:
1)如何取得年月日、小时分钟秒?
2)如何取得从1970年1月1日0时0分0秒到现在的毫秒数?
3)如何取得某月的最后一天?
4)如何格式化日期?
答:操作方法如下所示:
1)创建java.util.Calendar 实例,调用其get()方法传入不同的参数即可获得参数所对应的值
2)以下方法均可获得该毫秒数:
- Calendar.getInstance().getTimeInMillis();
- System.currentTimeMillis();
3)示例代码如下:
- Calendar time = Calendar.getInstance();
- time.getActualMaximum(Calendar.DAY_OF_MONTH);
4)利用java.text.DataFormat 的子类(如SimpleDateFormat类)中的format(Date)方法可将日期格式化。
42、打印昨天的当前时刻。
答:
- public class YesterdayCurrent {
- public static void main(String[] args){
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.DATE, -1);
- System.out.println(cal.getTime());
- }
- }
43、比较一下Java 和JavaSciprt。
答:JavaScript 与Java是两个公司开发的不同的两个产品。Java 是原Sun 公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;而JavaScript是Netscape公司的产品,为了扩展Netscape浏览器的功能而开发的一种可以嵌入Web页面中运行的基于对象和事件驱动的解释性语言,它的前身是LiveScript;而Java 的前身是Oak语言。
下面对两种语言间的异同作如下比较:
1)基于对象和面向对象:Java是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言。因而它本身提供了非常丰富的内部对象供设计人员使用;
2)解释和编译:Java 的源代码在执行之前,必须经过编译;JavaScript 是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行;
3)强类型变量和类型弱变量:Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript中变量声明,采用其弱类型。即变量在使用前不需作声明,而是解释器在运行时检查其数据类型;
4)代码格式不一样。
补充:上面列出的四点是原来所谓的标准答案中给出的。其实Java和JavaScript最重要的区别是一个是静态语言,一个是动态语言。目前的编程语言的发展趋势是函数式语言和动态语言。在Java中类(class)是一等公民,而JavaScript中函数(function)是一等公民。对于这种问题,在面试时还是用自己的语言回答会更加靠谱。
44、什么时候用assert?
答:assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。一般来说,assertion用于保证程序最基本、关键的正确性。assertion检查通常在开发和测试时开启。为了提高性能,在软件发布后, assertion检查通常是关闭的。在实现中,断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为true;如果表达式计算为false,那么系统会报告一个AssertionError。
断言用于调试目的:
assert(a > 0); // throws an AssertionError if a <= 0
断言可以有两种形式:
assert Expression1;
assert Expression1 : Expression2 ;
Expression1 应该总是产生一个布尔值。
Expression2 可以是得出一个值的任意表达式;这个值用于生成显示更多调试信息的字符串消息。
断言在默认情况下是禁用的,要在编译时启用断言,需使用source 1.4 标记:
javac -source 1.4 Test.java
要在运行时启用断言,可使用-enableassertions 或者-ea 标记。
要在运行时选择禁用断言,可使用-da 或者-disableassertions 标记。
要在系统类中启用断言,可使用-esa 或者-dsa 标记。还可以在包的基础上启用或者禁用断言。可以在预计正常情况下不会到达的任何位置上放置断言。断言可以用于验证传递给私有方法的参数。不过,断言不应该用于验证传递给公有方法的参数,因为不管是否启用了断言,公有方法都必须检查其参数。不过,既可以在公有方法中,也可以在非公有方法中利用断言测试后置条件。另外,断言不应该以任何方式改变程序的状态。
45、Error 和Exception 有什么区别?
答:Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。
补充:2005年摩托罗拉的面试中曾经问过这么一个问题“If a process reports a stack overflow run-time error, what’s the most possible cause?”,给了四个选项a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java程序在运行时也可能会遭遇StackOverflowError,这是一个错误无法恢复,只能重新修改代码了,这个面试题的答案是c。如果写了不能迅速收敛的递归,则很有可能引发栈溢出的错误,如下所示:
- package com.lovo;
-
- public class StackOverflowErrorTest {
-
- public static void main(String[] args) {
- main(null);
- }
- }
因此,用递归编写程序时一定要牢记两点:1. 递归公式;2. 收敛条件(什么时候就不再递归而是回溯了)。
46、try{}里有一个return语句,那么紧跟在这个try后的finally{}里的code会不会被执行,什么时候被执行,在return前还是后?
答:会执行,在方法返回调用者前执行。Java允许在finally中改变返回值的做法是不好的,因为如果存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值,然后如果在finally中修改了返回值,这会对程序造成很大的困扰,C#中就从语法上规定不能做这样的事。
47、Java 语言如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用?
答:Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java 中,每个异常都是一个对象,它是Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。Java 的异常处理是通过5 个关键词来实现的:try、catch、throw、throws和finally。一般情况下是用try来执行一段程序,如果出现异常,系统会抛出(throw)一个异常,这时候你可以通过它的类型来捕捉(catch)它,或最后(finally)由缺省处理器来处理;try用来指定一块预防所有“异常”的程序;catch 子句紧跟在try块后面,用来指定你想要捕捉的“异常”的类型;throw 语句用来明确地抛出一个“异常”;throws用来标明一个成员函数可能抛出的各种“异常”;finally 为确保一段代码不管发生什么“异常”都被执行一段代码;可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部写另一个try语句保护其他代码。每当遇到一个try 语句,“异常”的框架就放到栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种“异常”进行处理,栈就会展开,直到遇到有处理这种“异常”的try 语句。
48、运行时异常与受检异常有何异同?
答:异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对象程序设计中经常被滥用的东西,神作《Effective Java》中对异常的使用给出了以下指导原则:
- 不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常)
- 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
- 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
- 优先使用标准的异常
- 每个方法抛出的异常都要有文档
- 保持异常的原子性
- 不要在catch中忽略掉捕获到的异常
49、列出一些你常见的运行时异常?
答:
ArithmeticException(算术异常)
ClassCastException (类转换异常)
IllegalArgumentException (非法参数异常)
IndexOutOfBoundsException (下表越界异常)
NullPointerException (空指针异常)
SecurityException (安全异常)
50、final, finally, finalize 的区别?
答:final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final 的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final 的方法也同样只能使用,不能在子类中被重写。finally:通常放在try…catch的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。
Java程序员面试题集(51-70)
摘要:这一部分主要讲解了异常、多线程、容器和I/O的相关面试题。首先,异常机制提供了一种在不打乱原有业务逻辑的前提下,把程序在运行时可能出现的状况处理掉的优雅的解决方案,同时也是面向对象的解决方案。而Java的线程模型是建立在共享的、默认的可见的可变状态以及抢占式线程调度两个概念之上的。Java内置了对多线程编程的支持在20世纪90年代可以说是一个巨大的进步,但是最初的设计在当下看来已经给程序带来很多困扰了。感谢Doug Lea在Java5中提供了他里程碑式的杰作java.util.concurrent包,它的出现让Java的多线程编程能够更好的工作。Java 1.4中引入NIO实现了对非阻塞I/O的支持,NIO为I/O操作抽象出缓冲区和通道层,解决了字符集的编码和解码问题,提供了将文件映射为内存数据的接口。NIO无疑使Java向前迈出了一大步,但为了方便Java对文件系统的处理,NIO.2进一步对Java的I/O操作进行了增强,提供了能批量获取文件属性的文件系统接口,还提供了套接字和文件都能进行异步IO操作的API,完成了JSR-51中定义的套接字。对于Java中的容器(集合框架)而言,Java 5中引入泛型无疑是程序员的福音,然而那仅仅是糖衣语法,底层实现没有本质的差别,因此与C#相比,Java的泛型显得不那么让人痛快。
51、类ExampleA 继承Exception,类ExampleB 继承ExampleA。
有如下代码片断:
- try{
- throw new ExampleB("b")
- }catch(ExampleA e){
- System.out.println("ExampleA");
- }catch(Exception e){
- System.out.println("Exception");
- }
请问执行此段代码的输出是什么?
答:输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取ExampleA类型异常的catch块能够抓住try块中抛出的ExampleB类型的异常)
补充:比此题略复杂的一道面试题如下所示(此题的出处是《Java编程思想》),说出你的答案吧!
- class Annoyance extends Exception {}
- class Sneeze extends Annoyance {}
-
- class Human {
-
- public static void main(String[] args)
- throws Exception {
- try {
- try {
- throw new Sneeze();
- }
- catch ( Annoyance a ) {
- System.out.println("Caught Annoyance");
- throw a;
- }
- }
- catch ( Sneeze s ) {
- System.out.println("Caught Sneeze");
- return ;
- }
- finally {
- System.out.println("Hello World!");
- }
- }
- }
52、List、Set、Map 是否继承自Collection 接口?
答:List、Set 是,Map 不是。Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。
53、说出ArrayList、Vector、LinkedList 的存储性能和特性?
答:ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized 方法(线程安全),通常性能上较ArrayList 差,而LinkedList 使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,其实对内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。Vector属于遗留容器(早期的JDK中使用的容器,除此之外Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),现在已经不推荐使用,但是由于ArrayList和LinkedListed都是非线程安全的,如果需要多个线程操作同一个容器,那么可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这其实是装潢模式最好的例子,将已有对象传入另一个类的构造器中创建新的对象来增加新功能)。
补充:遗留容器中的Properties类和Stack类在设计上有严重的问题,Properties是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个Hashtable并将其两个泛型参数设置为String类型,但是Java API中的Properties直接继承了Hashtable,这很明显是对继承的滥用。这里复用代码的方式应该是HAS-A关系而不是IS-A关系,另一方面容器都属于工具类,继承工具类本身就是一个错误的做法,使用工具类最好的方式是HAS-A关系(关联)或USE-A关系(依赖)。同理,Stack类继承Vector也是不正确的。
54、Collection 和Collections 的区别?
答:Collection 是一个接口,它是Set、List等容器的父接口;Collections 是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。
55、List、Map、Set 三个接口,存取元素时,各有什么特点?
答:List以特定索引来存取元素,可有重复元素。Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。Map保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。
56、TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
答:TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是是通过接口注入比较元素大小的算法,也是对回调模式的应用。
例子1:
Student.java
- package com.lovo.demo;
-
- public class Student implements Comparable<Student> {
- private String name;
- private int age;
-
- public Student(String name, int age) {
- this.name = name;
- this.age = age;
- }
-
- @Override
- public String toString() {
- return "Student [name=" + name + ", age=" + age + "]";
- }
-
- @Override
- public int compareTo(Student o) {
- return this.age - o.age;
- }
-
- }
Test01.java
- package com.lovo.demo;
-
- import java.util.Set;
- import java.util.TreeSet;
-
- class Test01 {
-
- public static void main(String[] args) {
- Set<Student> set = new TreeSet<>();
- set.add(new Student("Hao LUO", 33));
- set.add(new Student("XJ WANG", 32));
- set.add(new Student("Bruce LEE", 60));
- set.add(new Student("Bob YANG", 22));
-
- for(Student stu : set) {
- System.out.println(stu);
- }
-
-
-
-
-
- }
- }
例子2:
Student.java
- package com.lovo.demo;
-
- public class Student {
- private String name;
- private int age;
-
- public Student(String name, int age) {
- this.name = name;
- this.age = age;
- }
-
-
-
-
- public String getName() {
- return name;
- }
-
-
-
-
- public int getAge() {
- return age;
- }
-
- @Override
- public String toString() {
- return "Student [name=" + name + ", age=" + age + "]";
- }
-
- }
Test02.java
- package com.lovo.demo;
-
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.Comparator;
- import java.util.List;
-
- class Test02 {
-
- public static void main(String[] args) {
- List<Student> list = new ArrayList<>();
- list.add(new Student("Hao LUO", 33));
- list.add(new Student("XJ WANG", 32));
- list.add(new Student("Bruce LEE", 60));
- list.add(new Student("Bob YANG", 22));
-
-
-
-
-
- Collections.sort(list, new Comparator<Student> () {
-
- @Override
- public int compare(Student o1, Student o2) {
- return o1.getName().compareTo(o2.getName());
- }
- });
-
- for(Student stu : list) {
- System.out.println(stu);
- }
-
-
-
-
-
- }
- }
57、sleep()和wait()有什么区别?
答:sleep()方法是线程类(Thread)的静态方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复(线程回到就绪(ready)状态),因为调用sleep 不会释放对象锁。wait()是Object 类的方法,对此对象调用wait()方法导致本线程放弃对象锁(线程暂停执行),进入等待此对象的等待锁定池,只有针对此对象发出notify 方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入就绪状态。
补充:这里似乎漏掉了一个作为先决条件的问题,就是什么是进程,什么是线程?为什么需要多线程编程?答案如下所示:
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它占用了更多的CPU资源。
58、sleep()和yield()有什么区别?
答:
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统相关)具有更好的可移植性。
59、当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法?
答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。
60、请说出与线程同步相关的方法。
答:
- wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
- sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException 异常;
- notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
- notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争;
- JDK 1.5通过Lock接口提供了显式(explicit)的锁机制,增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;
- JDK 1.5还提供了信号量(semaphore)机制,信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。
下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。
银行账户类:
- package com.lovo;
-
-
-
-
-
-
- public class Account {
- private double balance;
-
-
-
-
-
- public void deposit(double money) {
- double newBalance = balance + money;
- try {
- Thread.sleep(10);
- }
- catch(InterruptedException ex) {
- ex.printStackTrace();
- }
- balance = newBalance;
- }
-
-
-
-
- public double getBalance() {
- return balance;
- }
- }
存钱线程类:
- package com.lovo;
-
-
-
-
-
-
- public class AddMoneyThread implements Runnable {
- private Account account;
- private double money;
-
- public AddMoneyThread(Account account, double money) {
- this.account = account;
- this.money = money;
- }
-
- @Override
- public void run() {
- account.deposit(money);
- }
-
- }
测试类:
- package com.lovo;
-
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- public class Test01 {
-
- public static void main(String[] args) {
- Account account = new Account();
- ExecutorService service = Executors.newFixedThreadPool(100);
-
- for(int i = 1; i <= 100; i++) {
- service.execute(new AddMoneyThread(account, 1));
- }
-
- service.shutdown();
-
- while(!service.isTerminated()) {}
-
- System.out.println("账户余额: " + account.getBalance());
- }
- }
在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:
1. 在银行账户的存款(deposit)方法上同步(synchronized)关键字
- package com.lovo;
-
-
-
-
-
-
- public class Account {
- private double balance;
-
-
-
-
-
- public synchronized void deposit(double money) {
- double newBalance = balance + money;
- try {
- Thread.sleep(10);
- }
- catch(InterruptedException ex) {
- ex.printStackTrace();
- }
- balance = newBalance;
- }
-
-
-
-
- public double getBalance() {
- return balance;
- }
- }
2. 在线程调用存款方法时对银行账户进行同步
- package com.lovo;
-
-
-
-
-
-
- public class AddMoneyThread implements Runnable {
- private Account account;
- private double money;
-
- public AddMoneyThread(Account account, double money) {
- this.account = account;
- this.money = money;
- }
-
- @Override
- public void run() {
- synchronized (account) {
- account.deposit(money);
- }
- }
-
- }
3. 通过JDK 1.5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
- package com.lovo;
-
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
-
-
-
-
-
-
-
- public class Account {
- private Lock accountLock = new ReentrantLock();
- private double balance;
-
-
-
-
-
-
-
- public void deposit(double money) {
- accountLock.lock();
- try {
- double newBalance = balance + money;
- try {
- Thread.sleep(10);
- }
- catch (InterruptedException ex) {
- ex.printStackTrace();
- }
- balance = newBalance;
- }
- finally {
- accountLock.unlock();
- }
- }
-
-
-
-
- public double getBalance() {
- return balance;
- }
- }
按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。
61、编写多线程程序有几种实现方式?
答:Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。
补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法可以在线程执行结束时产生一个返回值,代码如下所示:
- package com.lovo.demo;
-
- import java.util.ArrayList;
- import java.util.List;
- import java.util.concurrent.Callable;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Future;
-
-
- class MyTask implements Callable<Integer> {
- private int upperBounds;
-
- public MyTask(int upperBounds) {
- this.upperBounds = upperBounds;
- }
-
- @Override
- public Integer call() throws Exception {
- int sum = 0;
- for(int i = 1; i <= upperBounds; i++) {
- sum += i;
- }
- return sum;
- }
-
- }
-
- public class Test {
-
- public static void main(String[] args) throws Exception {
- List<Future<Integer>> list = new ArrayList<>();
- ExecutorService service = Executors.newFixedThreadPool(10);
- for(int i = 0; i < 10; i++) {
- list.add(service.submit(new MyTask((int) (Math.random() * 100))));
- }
-
- int sum = 0;
- for(Future<Integer> future : list) {
- while(!future.isDone()) ;
- sum += future.get();
- }
-
- System.out.println(sum);
- }
- }
62、synchronized关键字的用法?
答:synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第60题的例子中已经展示了synchronized关键字的用法。
63、举例说明同步和异步。
答:如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的悲观锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。
64、启动一个线程是用run()还是start()方法?
答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。
65、什么是线程池(thread pool)?
答:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是"池化资源"技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
- newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
第60题的例子中有通过Executors工具类创建线程池并使用线程池执行线程的代码。
如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
66、线程的基本状态以及状态之间的关系?
答:
除去起始(new)状态和结束(finished)状态,线程有三种状态,分别是:就绪(ready)、运行(running)和阻塞(blocked)。其中就绪状态代表线程具备了运行的所有条件,只等待CPU调度(万事俱备,只欠东风);处于运行状态的线程可能因为CPU调度(时间片用完了)的原因回到就绪状态,也有可能因为调用了线程的yield方法回到就绪状态,此时线程不会释放它占有的资源的锁,坐等CPU以继续执行;运行状态的线程可能因为I/O中断、线程休眠、调用了对象的wait方法而进入阻塞状态(有的地方也称之为等待状态);而进入阻塞状态的线程会因为休眠结束、调用了对象的notify方法或notifyAll方法或其他线程执行结束而进入就绪状态。注意:调用wait方法会让线程进入等待池中等待被唤醒,notify方法或notifyAll方法会让等待锁中的线程从等待池进入等锁池,在没有得到对象的锁之前,线程仍然无法获得CPU的调度和执行。
67、简述synchronized 和java.util.concurrent.locks.Lock的异同?
答:Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock 有比synchronized 更精确的线程语义和更好的性能。synchronized 会自动释放锁,而Lock 一定要求程序员手工释放,并且必须在finally 块中释放(这是释放外部资源的最好的地方)。
68、Java中如何实现序列化,有什么意义?
答:序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object obj)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆(参见Java面试题集1-29题)
69、Java 中有几种类型的流?
答:字节流,字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。
补充:关于Java的IO需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外Java中的流不同于C#的是它只有一个维度一个方向。
补充:下面用IO和NIO两种方式实现文件拷贝,这个题目在面试的时候是经常被问到的。
- package com.lovo;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.nio.ByteBuffer;
- import java.nio.channels.FileChannel;
-
- public class MyUtil {
-
- private MyUtil() {
- throw new AssertionError();
- }
-
- public static void fileCopy(String source, String target) throws IOException {
- try (InputStream in = new FileInputStream(source)) {
- try (OutputStream out = new FileOutputStream(target)) {
- byte[] buffer = new byte[4096];
- int bytesToRead;
- while((bytesToRead = in.read(buffer)) != -1) {
- out.write(buffer, 0, bytesToRead);
- }
- }
- }
- }
-
- public static void fileCopyNIO(String source, String target) throws IOException {
- try (FileInputStream in = new FileInputStream(source)) {
- try (FileOutputStream out = new FileOutputStream(target)) {
- FileChannel inChannel = in.getChannel();
- FileChannel outChannel = out.getChannel();
- ByteBuffer buffer = ByteBuffer.allocate(4096);
- while(inChannel.read(buffer) != -1) {
- buffer.flip();
- outChannel.write(buffer);
- buffer.clear();
- }
- }
- }
- }
- }
注意:上面用到Java 7的TWR,使用TWR后可以不用在finally中释放外部资源 ,从而让代码更加优雅。
70、写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。
答:代码如下:
- package com.lovo.demo;
-
- import java.io.BufferedReader;
- import java.io.FileReader;
-
- public class MyUtil {
-
-
- private MyUtil() {
- throw new AssertionError();
- }
-
-
-
-
-
-
-
-
- public static int countWordInFile(String filename, String word) {
- int counter = 0;
- try (FileReader fr = new FileReader(filename)) {
- try (BufferedReader br = new BufferedReader(fr)) {
- String line = null;
- while ((line = br.readLine()) != null) {
- int index = -1;
- while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) {
- counter++;
- line = line.substring(index + word.length());
- }
- }
- }
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- return counter;
- }
-
- }
Java程序员面试题集(71-85)
摘要:这一部分主要包括了UML(统一建模语言)、面向对象的设计原则(六原则一法则)、GoF设计模式、企业级设计模式、JDBC(Java数据库连接)、XML(可扩展标记语言)等知识。
71、UML是什么?UML中有哪些图?
答:UML是统一建模语言(Unified Modeling Language)的缩写,它发表于1997年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持。使用UML可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系统的结构和行为。UML定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构,包括:用例图(use case diagram)、类图(class diagram)、时序图(sequence diagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deployment diagram)等。在这些图形化符号中,有三种图最为重要,分别是:用例图(用来捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系)、类图(描述类以及类与类之间的关系,通过该图可以快速了解系统)、时序图(描述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能接收的消息也就是说对象能够向外界提供的服务)。
用例图:
类图:
时序图:
72、写一个单例类。
答:单例模式主要作用是保证在Java应用程序中,一个类只有一个实例存在。下面给出两种不同形式的单例:
第一种形式:饿汉式单例
- public class Singleton {
- private Singleton(){}
- private static Singleton instance = new Singleton();
- public static Singleton getInstance(){
- return instance;
- }
- }
第二种形式:懒汉式单例
- public class Singleton {
- private static Singleton instance = null;
- private Singleton() {}
- public static synchronized Singleton getInstance(){
- if (instance==null) instance=newSingleton();
- return instance;
- }
- }
单例的特点:外界无法通过构造器来创建对象,该类必须提供一个静态方法向外界提供该类的唯一实例。
【补充】用Java进行服务器端编程时,使用单例模式的机会还是很多的,服务器上的资源都是很宝贵的,对于那些无状态的对象其实都可以单例化或者静态化(在内存中仅有唯一拷贝),如果使用了spring这样的框架来进行对象托管,Spring的IoC容器在默认情况下对所有托管对象都是进行了单例化处理的。
73、说说你所熟悉或听说过的设计模式以及你对设计模式的看法。
答:在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中给出了三类(创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化])共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(责任链模式)。
所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。
【补充】设计模式并不是像某些地方吹嘘的那样是遥不可及的编程理念,说白了设计模式就是对面向对象的编程原则的实践,面向对象的编程原则包括:
- 单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是“高内聚”,写代码最终极的原则只有六个字“高内聚、低耦合”,就如同葵花宝典或辟邪剑谱的中心思想就八个字“欲练此功必先自宫”,所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。我们都知道一句话叫“因为专注,所以专业”,一个对象如果承担太多的职责,那么注定它什么都做不好。这个世界上任何好的东西都有两个特征,一个是功能单一,好的相机绝对不是电视购物里面卖的那种一个机器有一百多种功能的,它基本上只能照相;另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
- 开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱,如果不清楚如何封装可变性,可以参考《设计模式精解》一书中对桥梁模式的讲解的章节。)
- 依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的里氏替换原则。)
- 里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的描述,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对象来存储数据。记住:任何时候都不要继承工具类,工具是可以拥有并可以使用的(HAS/USE),而不是拿来继承的。)
- 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。(迪米特法则简单的说就是如何做到“低耦合”,门面模式和调停者模式就是对迪米特法则的践行。对于门面模式可以举一个简单的例子,你去一家公司洽谈业务,你不需要了解这个公司内部是如何运作的,你甚至可以对这个公司一无所知,去的时候只需要找到公司入口处的前台美女,告诉她们你要做什么,她们会找到合适的人跟你接洽,前台的美女就是公司这个系统的门面。再复杂的系统都可以为用户提供一个简单的门面,Java Web开发中作为前端控制器的Servlet或Filter不就是一个门面吗,浏览器对服务器的运作方式一无所知,但是通过前端控制器就能够根据你的请求得到相应的服务。调停者模式也可以举一个简单的例子来说明,例如一台计算机,CPU、内存、硬盘、显卡、声卡各种设备需要相互配合才能很好的工作,但是如果这些东西都直接连接到一起,计算机的布线将异常复杂,在这种情况下,主板作为一个调停者的身份出现,它将各个设备连接在一起而不需要每个设备之间直接交换数据,这样就减小了系统的耦合度和复杂度。迪米特法则用通俗的话来将就是不要和陌生人打交道,如果真的需要,找一个自己的朋友,让他替你和陌生人打交道。)
74、Java企业级开发中常用的设计模式有哪些?
答: 按照分层开发的观点,可以将应用划分为:表示层、业务逻辑层和持久层,每一层都有属于自己类别的设计模式。
表示层设计模式:
1)Interceptor Filter:拦截过滤器,提供请求预处理和后处理的方案,可以对请求和响应进行过滤。
2)Front Controller:通过中央控制器提供请求管理和处理,管理内容读取、安全性、视图管理和导航等功能。Struts 2中的StrutsPrepareAndExecuteFilter、Spring MVC中的DispatcherServlet都是前端控制器,后者如下图所示:
3)View Helper:视图帮助器,负责将显示逻辑和业务逻辑分开。显示的部分放在视图组件中,业务逻辑代码放在帮助器中,典型的功能是内容读取、验证与适配。
4)Composite View:复合视图。
业务逻辑层设计模式:
1)Business Delegate:业务委托,减少表示层和业务逻辑层之间的耦合。
2)Value Object:值对象,解决层之间交换数据的开销问题。
3)Session Façade:会话门面,隐藏业务逻辑组件的细节,集中工作流程。
4)Value Object Assembler:灵活的组装不同的值对象
5)Value List Handler:提供执行查询和处理结果的解决方案,还可以缓存查询结果,从而达到提升性能的目的。
6)Service Locator:服务定位器,可以查找、创建和定位服务工厂,封装其实现细节,减少复杂性,提供单个控制点,通过缓存提高性能。
持久层设计模式:
1)Data Access Object:数据访问对象,以面向对象的方式完成对数据的增删改查。
【补充】如果想深入的了解Java企业级应用的设计模式和架构模式,可以参考这些书籍: 《Pro Java EE Spring Patterns》、《POJO in Action》、《Patterns of Enterprise Application Architecture》。
75、你在开发中都用到了那些设计模式?用在什么场合?
答:面试被问到关于设计模式的知识时,可以拣最常用的作答,例如:
1)工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
2)代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache代理、防火墙代理、同步化代理、智能引用代理。
3)适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。
4)模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。
除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式(Collections工具类里面的synchronizedXXX方法把一个线程不安全的容器变成线程安全容器就是对装潢模式的应用,而JavaIO里面的过滤流(有的翻译成处理流)也是应用装潢模式的经典例子)等,反正原则就是拣自己最熟悉的用得最多的作答,以免言多必失。
76、XML 文档定义有几种形式?它们之间有何本质区别?解析XML 文档有哪几种方式?
答:XML文档定义分为DTD和Schema两种形式;其本质区别在于Schema本身也是一个XML文件,可以被XML解析器解析。对XML的解析主要有DOM(文档对象模型)、SAX、StAX(JDK 1.6中引入的新的解析XML的方式,Streaming API for XML) 等,其中DOM处理大型文件时其性能下降的非常厉害,这个问题是由DOM 的树结构所造成的,这种结构占用的内存较多,而且DOM 必须在解析文件之前把整个文档装入内存,适合对XML 的随机访问(典型的用空间换取时间的策略);SAX是事件驱动型的XML解析方式,它顺序读取XML文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过在其回调事件中写入处理代码来处理XML文件,适合对XML 的顺序访问;如其名称所暗示的那样,StAX把重点放在流上。实际上,StAX与其他方法的区别就在于应用程序能够把XML作为一个事件流来处理。将XML作为一组事件来处理的想法并不新颖(事实上 SAX 已经提出来了),但不同之处在于StAX允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。
77、你在项目中哪些地方用到了XML?
答:XML的主要作用有两个方面:数据交换(曾经被称为业界数据交换的事实标准,现在此项功能在很多时候都被JSON取代)和信息配置。在做数据交换时,XML将数据用标签组装成起来,然后压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再从XML文件中还原相关信息进行处理。目前很多软件都使用XML来存储配置信息,很多项目中我们通常也会将作为配置的硬代码(hard code)写在XML文件中,Java的很多框架也是这么做的。
78、在进行数据库编程时,连接池有什么作用?
答:由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行TCP的三次握手,再加上网络延迟,造成的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。池化技术在Java开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于Java的开源数据库连接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
【补充】在计算机系统中时间和空间是不可调和的矛盾,理解这一点对设计满足性能要求的算法是至关重要的。大型网站性能优化的一个关键就是使用缓存,而缓存跟上面讲的连接池道理非常类似,也是使用空间换时间的策略。可以将热点数据置于缓存中,当用户查询这些数据时可以直接从缓存中得到,这无论如何也快过去数据库中查询。当然,缓存的置换策略等也会对系统性能产生重要影响,对于这个问题的讨论已经超出了这里要阐述的范围。
79、什么是DAO模式?
答:DAO(DataAccess Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露数据库实现细节的前提下提供了各种数据操作。为了建立一个健壮的Java EE应用,应该将所有对数据源的访问操作进行抽象化后封装在一个公共API中。用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO模式实际上包含了两个模式,一是Data Accessor(数据访问器),二是Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。
80、什么是ORM?
答:对象关系映射(Object-Relational Mapping,简称ORM)是一种为了解决程序的面向对象模型与数据库的关系模型互不匹配问题的技术;简单的说,ORM 是通过使用描述对象和数据库之间映射的元数据(可以用XML或者是注解),将Java程序中的对象自动持久化到关系数据库中或者将关系数据库表中的行转换成Java对象,其本质上就是将数据从一种形式转换到另外一种形式。
81、JDBC中如何进行事务处理?
答:Connection提供了事务处理的方法,通过调用setAutoCommit(false)可以设置手动提交事务;当事务完成后用commit()显式提交事务;如果在事务处理过程中发生异常则通过rollback()进行事务回滚。除此之外,较新的JDBC标准还引入了Savepoint(保存点)的概念,允许事务回滚到指定的保存点。
82、 事务的ACID是指什么?
答:
1)原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
2)一致性(Consistent):事务结束后系统状态是一致的;
3)隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
4)持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。
【补充】关于事务,在面试中被问到的概率是很高的,可以问的问题也是很多的。首先需要知道的是,只有存在并发数据访问时才需要事务。当多个事务访问同一数据时,可能会存在5类问题,包括3类数据读取问题(脏读、不可重复读和幻读)和2类数据更新问题(第1类丢失更新和第2类丢失更新)。
- 脏读(Dirty Read):A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。
时间 |
转账事务A |
取款事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
|
查询账户余额为1000元 |
T4 |
|
取出500元余额修改为500元 |
T5 |
查询账户余额为500元(脏读) |
|
T6 |
|
撤销事务余额恢复为1000元 |
T7 |
汇入100元把余额修改为600元 |
|
T8 |
提交事务 |
|
- 不可重复读(Unrepeatable Read):事务A重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务B修改过了。
时间 |
转账事务A |
取款事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
|
查询账户余额为1000元 |
T4 |
查询账户余额为1000元 |
|
T5 |
|
取出100元修改余额为900元 |
T6 |
|
提交事务 |
T7 |
查询账户余额为900元(不可重复读) |
|
- 幻读(Phantom Read):事务A重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务B提交的行。
时间 |
统计金额事务A |
转账事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
统计总存款为10000元 |
|
T4 |
|
新增一个存款账户存入100元 |
T5 |
|
提交事务 |
T6 |
再次统计总存款为10100元(幻读) |
|
- 第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。
时间 |
取款事务A |
转账事务B |
T1 |
开始事务 |
|
T2 |
|
开始事务 |
T3 |
查询账户余额为1000元 |
|
T4 |
|
查询账户余额为1000元 |
T5 |
|
汇入100元修改余额为1100元 |
T6 |
|
提交事务 |
T7 |
取出100元将余额修改为900元 |
|
T8 |
撤销事务 |
|
T9 |
余额恢复为1000元(丢失更新) |
|
- 第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。
时间 |
转账事务A |
取款事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
|
查询账户余额为1000元 |
T4 |
查询账户余额为1000元 |
|
T5 |
|
取出100元将余额修改为900元 |
T6 |
|
提交事务 |
T7 |
汇入100元将余额修改为1100元 |
|
T8 |
提交事务 |
|
T9 |
查询账户余额为1100元(丢失更新) |
|
数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,按锁定对象不同可以分为表级锁和行级锁;按并发事务锁定关系可以分为共享锁和独占锁,具体的内容大家可以自行查阅资料进行了解。
直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别,数据库就会通过分析SQL语句然后为事务访问的资源加上合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的(就是说你不用理解,事实上我确实也不知道)。ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,如下表所示:
隔离级别 |
脏读 |
不可重复读 |
幻读 |
第一类丢失更新 |
第二类丢失更新 |
READ UNCOMMITED |
允许 |
允许 |
允许 |
不允许 |
允许 |
READ COMMITTED |
不允许 |
允许 |
允许 |
不允许 |
允许 |
REPEATABLE READ |
不允许 |
不允许 |
允许 |
不允许 |
不允许 |
SERIALIZABLE |
不允许 |
不允许 |
不允许 |
不允许 |
不允许 |
需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。
83、Statement和PreparedStatement有什么区别?哪个性能更好?
答: 与Statement相比,①PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL的编译错误并增加SQL的安全性(减少SQL注射攻击的可能性);②PreparedStatement中的SQL语句是可以带参数的;③当批量处理SQL时或频繁执行相同的查询时,PreparedStatement有明显的性能上的优势,由于数据库可以将编译优化后的SQL语句缓存起来,下次执行相同语句时就会很快。
【补充】为了提供对存储过程的调用,JDBC API中还提供了CallableStatement接口。存储过程(Stored Procedure)是数据库系统中,一组为了完成特定功能的SQL语句的集合,经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。虽然调用存储过程会带来网络开销、安全性、性能上的好处,但是存在如果底层数据库发生迁移时就会有很多麻烦,因为每种数据库的存储过程在书写上存在不少的差别。
84、使用JDBC操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能?
答:要提升读取数据的性能,可以指定通过结果集(ResultSet)对象指定每次抓取数据的大小(fetch size);要提升更新数据的性能可以使用PreparedStatement语句构建批处理(batch)。
85、JDBC能否处理Blob和Clob?
答: 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代码向数据库中插入一条记录:
- package com.lovo.demo;
-
- 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 {
-
- Class.forName("com.mysql.jdbc.Driver");
-
- con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
-
- PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)");
- ps.setString(1, "骆昊");
- try (InputStream in = new FileInputStream("test.jpg")) {
- ps.setBinaryStream(2, in);
-
- System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失败");
- } catch(IOException e) {
- System.out.println("读取照片失败!");
- }
- } catch (ClassNotFoundException | SQLException e) {
- e.printStackTrace();
- } finally {
- try {
- if(con != null && !con.isClosed()) {
- con.close();
- con = null;
- }
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }
- }
Java程序员面试题集(86-115)
摘要:下面的内容包括Struts 2和hibernate的常见面试题,虽然Struts 2在2013年6月曝出高危漏洞后已经显得江河日下,而spring MVC的异军突起更加加速了Struts 2的陨落,但面试中仍然有可能被问及和此框架相关的内容,毕竟Struts 2曾经被阿里巴巴、京东以及政府企业门户网站广泛采用。另一方面,Hibernate目前仍然是ORM框架中的中坚力量,MyBatis在此领域也有不容忽视的一席之地,因此了解这两个ORM框架对Java程序员是很有必要的。第一期发布的Java面试题集中的150题并未包含Spring MVC和MyBatis的内容,后面会陆续为大家奉上。
86、Struts 2中,Action通过什么方式获得用户从页面输入的数据,又是通过什么方式把其自身的数据传给视图的?
答:Action从页面获取数据有三种方式:
①通过Action属性接受参数
②通过域模型获取参数
③通过模型驱动获取参数 (ModelDriven<T>)
Action将数据存入值栈(Value Stack)中,视图可以通过表达式语言(EL)从值栈中获取数据。
87、简述Struts 2是如何实现MVC架构模式的。
答:MVC架构模式要求应用程序的输入、处理和输出三者分离,将系统分成模型(Model)、视图(View)、控制器(Controller)三个部分,通过控制器实现模型和视图的解耦合,使得应用程序的开发和维护变得容易,如下图所示。其中,模型代表了应用程序的数据和处理这些数据的规则,同时还可以为视图提供的查询保存相关的状态,通常由JavaBean来实现,模型的代码写一次就可以被多个视图重用;视图用来组织模型的内容,它从模型中获得数据,并将数据展现给用户,在Struts 2中通常由JSP、Freemarker模板等来实现;控制器负责从客户端接受请求并将其转换为某种行为,行为完成后再选择一个视图来呈现给用户,控制器本身不需要输出任何内容,它只是接收请求并决定调用哪个模型组件去处理请求,StrutsPrepareAndExecuteFilter过滤器是Struts 2中的核心,它和一系列的Action构成了Struts 2中的控制器。
图-1 MVC架构模式图
88、阐述Struts 2如何实现用户输入验证。在你做过的项目中使用的是那种验证方式,为什么选择这种方式?
答:Struts 2可以使用手动验证和自动验证框架实现用户输入验证。自动验证框架是将对输入的验证规则放在XML文件中,这种方式比较灵活,可以在不修改代码的情况下修改验证的规则。
89、阐述Struts 2中的Action如何编写?Action是否采用了单例?
答:Struts2的Action有三种写法:
①POJO
②实现Action接口重写execute()方法
③继承ActionSupport类
Action没有像Servlet一样使用单实例多线程的工作方式,很明显,每个Action要接收不同用户的请求参数,这就意味着Action是有状态的,因此在设计上使用了每个请求对应一个Action的处理方式。
90、Struts 2中的Action并没有直接收到用户的请求,那它为什么可以处理用户的请求,又凭什么知道一个请求到底交给哪个Action来处理?
答:Struts2的核心过滤器接收到用户请求后,会对用户的请求进行简单的预处理(例如解析、封装参数),然后通过反射来创建Action实例,并调用Action中指定的方法来处理用户请求。
要决定请求交给哪一个Action来处理有两种方式:1利用配置文件:可以在配置文件中通过<action>标签配置和请求对应的Action类以及要调用的方法;2利用约定:Struts2中可以使用约定(convention)插件,例如约定xxx总是对应XxxAction,这是对约定优于配置理念的践行。
91、你经常用到的Struts 2常量配置有哪些?
答:
①struts.i18n.encoding– 指定默认编码
②struts.objectFactory/ struts.objectFactory.spring.autoWire – 对象工厂 / Spring的自动装配方式(名字、类型)
③struts.devMode– 是否使用开发模式
④struts.locale– 指定默认区域,默认值是en_US
⑤struts.i18n.resources– 国际化使用的资源文件
⑥struts.enable.DynamicMethodInvocation– 是否允许动态方法调用
92、简述Struts2的异常处理机制。
答:Struts 2提供了声明式的异常处理机制,可以在配置文件中加入如下代码:
<global-exception-mappings>
<exception-mapping exception=”…” result=”…”/>
</global-exception-mappings>
93、说一下你对约定优于配置(CoC)的理解。
答:约定优于配置(convention over configuration),也称作按约定编程,是一种软件设计范式,旨在减少软件开发人员需做决定的数量,获得简单的好处而又不失灵活性。CoC本质是说,开发人员仅需规定应用中不符约定的部分。例如,如果模型中有个名为Sale的类,那么数据库中对应的表就会默认命名为sales。只有在偏离这一约定时,例如将该表命名为products_sold,才需写有关这个名字的配置。如果您所用工具的约定与你的期待相符,便可省去配置;反之,你可以配置来达到你所期待的方式。遵循约定虽然损失了一定的灵活性,不能随意安排目录结构,不能随意进行函数命名,但是却能减少配置。更重要的是,遵循约定可以帮助开发人员遵守构建标准,包括各种命名的规范,这对团队开发是非常有利的。
94、Struts2中如何实现I18N?
答:首先,为不同语言地区编写不同的资源文件;然后在Struts 2配置文件中配置struts.i18n.custom.resources常量;在Action中可以通过调用getText()方法读取资源文件获取国际化资源。
95、简述拦截器的工作原理以及你在项目中使用过哪些自定义拦截器。
答:Struts 2中定义了拦截器的接口以及默认实现,实现了Interceptor接口或继承了AbstractInterceptor的类可以作为拦截器。接口中的init()方法在拦截器被创建后立即被调用,它在拦截器的生命周期内只被调用一次,可以在该方法中对相关资源进行必要的初始化。每拦截一个请求,intercept()方法就会被调用一次。destory()方法将在拦截器被销毁之前被调用, 它在拦截器的生命周期内也只被调用一次。
项目中使用过的有权限拦截器、执行时间拦截器、令牌拦截器等。
96、如何在Struts2中使用Ajax功能?
答:以下是Struts 2中实现Ajax的可选方式:
①JSON plugin+ jQuery
②DOJO plugin
③DWR (DirectWeb Remoting)
97、谈一下拦截器和过滤器的区别。
答:拦截器和过滤器都可以用来实现横切关注功能,其区别主要在于:
①拦截器是基于Java反射机制的,而过滤器是基于接口回调的。
②过滤器依赖于Servlet容器,而拦截器不依赖于Servlet容器。
③拦截器只能对Action请求起作用,而过滤器可以对所有请求起作用。
④拦截器可以访问Action上下文、值栈里的对象,而过滤器不能。
98、谈一下Struts 1和Struts 2的区别。
答:
99、谈一下你的项目选择Struts 2的理由
答:①Action是POJO,没有依赖Servlet API,具有良好的可测试性;②强大的拦截器简化了开发的复杂度;③支持多种表现层技术:JSP、Freemarker等等;④灵活的验证方式;⑤国际化(I18N)支持;⑥声明式异常管理;⑦通过JSON插件简化Ajax;⑧通过Spring插件跟Spring整合。
【补充】有人为选择和评判Web框架提出了20条标准,包括:开发人员的工作效率(能用1-5天搭建一个CRUD页面吗)、开发人员的看法(用起来有意思吗)、学习曲线(学了一个星期或一个月后能干活吗)、项目健康状况(项目陷入绝境了吗)、开发人员的充足性(能找到经验丰富的开发人员吗)、就业趋势(将来能招到人吗)、模板化(遵循DRY原则吗)、组件(自带日期选择器之类的控件吗)、Ajax(是否支持异步调用和局部刷新)、插件或附加项(能加入Facebook集成之类的功能吗)、扩展性(默认的控制处理的并发用户数能到500+吗)、测试支持(能够做测试驱动的开发吗)、I18N和L10N(有多国语言、地域支持吗)、校验(能轻松校验用户输入并迅速反馈吗)、多编程语言支持(能够同时使用多种语言开发吗)、文档的质量(常见的用例和问题都在文档中有体现吗)、出版的图书(有没有行业专家使用了它并分享了自己的使用经验)、REST支持(能按HTTP协议的设计宗旨使用该协议吗)、移动支持(是否很容易就能支持Android、iOS和其他移动智能终端)、风险程度(能不能做大型项目)。很明显,Java其实算不上最优秀的Web开发语言,但是它却满足了这20条中的很多,尤其是充足的开发人员、成熟的解决方案这两点,而且Java的生态系统是非常良好的,这也是在Java已经显得江河日下的今天大家仍然一如既往的将其作为开发语言首选的原因。
100、Struts 2中如何访问HttpServletRequest、HttpSession和ServletContext三个域对象
答:有两种方式:
①通过ServletActionContext的方法获得;
②通过ServletRequestAware、SessionAware和ServletContextAware接口注入。
101、Struts 2中的默认包struts-default有什么作用?
答:它定义了Struts 2内部的众多拦截器和Result类型,而Struts 2很多核心的功能都是通过这些内置的拦截器实现,如:从请求中把请求参数封装到action、文件上传和数据验证等等都是通过拦截器实现的。在Struts 2的配置文件中,自定义的包继承了struts-default包就可以使用Struts 2为我们提供的这些功能。
102、简述值栈(Value-Stack)的原理和生命周期
答:Value-Stack贯穿整个 Action 的生命周期,保存在request作用域中,所以它和request的生命周期一样。当Struts 2接受一个请求时,会创建ActionContext、Value-Stack和Action对象,然后把Action存放进Value-Stack,所以Action的实例变量可以通过OGNL访问。由于Action是多实例的,和使用单例的Servlet不同, 每个Action都有一个对应的Value-Stack,Value-Stack存放的数据类型是该Action的实例,以及该Action中的实例变量,Action对象默认保存在栈顶。
103、SessionFactory是线程安全的吗?Session是线程安全的吗,两个线程能够共享同一个Session吗?
答:SessionFactory对应Hibernate的一个数据存储的概念,它是线程安全的,可以被多个线程并发访问。SessionFactory一般只会在启动的时候构建。对于应用程序,最好将SessionFactory通过单例的模式进行封装以便于访问。Session是一个轻量级非线程安全的对象(线程间不能共享session),它表示与数据库进行交互的一个工作单元。Session是由SessionFactory创建的,在任务完成之后它会被关闭。Session是持久层服务对外提供的主要接口。Session会延迟获取数据库连接(也就是在需要的时候才会获取)。为了避免创建太多的session,可以使用ThreadLocal来取得当前的session,无论你调用多少次getCurrentSession()方法,返回的都是同一个session。
104、Session的load和get方法的区别是什么?
答:主要有以下三项区别:
① 如果没有找到符合条件的记录, get方法返回null,load方法抛出异常
②get方法直接返回实体类对象, load方法返回实体类对象的代理
③ 在Hibernate 3之前,get方法只在一级缓存(内部缓存)中进行数据查找, 如果没有找到对应的数据则越过二级缓存, 直接发出SQL语句完成数据读取; load方法则可以充分利用二级缓存中的现有数据;当然从Hibernate 3开始,get方法不再是对二级缓存只写不读,它也是可以访问二级缓存的
简单的说,对于load()方法Hibernate认为该数据在数据库中一定存在可以放心的使用代理来实现延迟加载,如果没有数据就抛出异常,而通过get()方法去取的数据可以不存在。
105、Session的save()、update()、merge()、lock()、saveOrUpdate()和persist()方法有什么区别?
答:Hibernate的对象有三种状态:瞬态、持久态和游离态。游离状态的实例可以通过调用save()、persist()或者saveOrUpdate()方法进行持久化;脱管状态的实例可以通过调用 update()、0saveOrUpdate()、lock()或者replicate()进行持久化。save()和persist()将会引发SQL的INSERT语句,而update()或merge()会引发UPDATE语句。save()和update()的区别在于一个是将瞬态对象变成持久态,一个是将游离态对象变为持久态。merge方法可以完成save()和update()方法的功能,它的意图是将新的状态合并到已有的持久化对象上或创建新的持久化对象。按照官方文档的说明:(1)persist()方法把一个瞬态的实例持久化,但是并"不保证"标识符被立刻填入到持久化实例中,标识符的填入可能被推迟到flush的时间;(2) persist"保证",当它在一个事务外部被调用的时候并不触发一个Insert语句,当需要封装一个长会话流程的时候,一个persist这样的函数是需要的。(3)save"不保证"第2条,它要返回标识符,所以它会立即执行Insert语句,不管是不是在事务内部还是外部。update()方法是把一个已经更改过的脱管状态的对象变成持久状态;lock()方法是把一个没有更改过的脱管状态的对象变成持久状态。
106、阐述Session加载实体对象的过程。
答:Session加载实体对象的步骤是:
① Session在调用数据库查询功能之前, 首先会在缓存中进行查询, 在一级缓存中, 通过实体类型和主键进行查找, 如果一级缓存查找命中且数据状态合法, 则直接返回
③ 如果一级缓存没有命中, 接下来Session会在当前NonExists记录(相当于一个查询黑名单, 如果出现重复的无效查询可以迅速判断, 从而提升性能)中进行查找, 如果NonExists中存在同样的查询条件,则返回null
③ 对于load方法, 如果一级缓存查询失败则查询二级缓存, 如果二级缓存命中则直接返回
④ 如果之前的查询都未命中, 则发出SQL语句, 如果查询未发现对应记录则将此次查询添加到Session的NonExists中加以记录, 并返回null
⑤ 根据映射配置和SQL语句得到ResultSet,并创建对应的实体对象
⑥ 将对象纳入Session(一级缓存)管理
⑦ 执行拦截器的onLoad方法(如果有对应的拦截器)
⑧将数据对象纳入二级缓存
⑨返回数据对象
107、Query接口的list方法和iterate方法有什么区别?
答:
1) list方法无法利用缓存,它对缓存只写不读; iterate方法可以充分利用缓存, 如果目标数据只读或者读取频繁, iterate可以减少性能开销
2) list方法不会引起N+1查询问题, 而iterate方法会引起N+1查询问题
108、Hibernate如何实现分页查询?
答:通过Hibernate实现分页查询,开发人员只需要提供HQL语句、查询起始行数(setFirstresult()方法)和最大查询行数(setMaxResult()方法),并调用Query接口的list()方法,Hibernate会自动生成分页查询的SQL语句。
109、锁机制有什么用?简述Hibernate的悲观锁和乐观锁机制。
答:有些业务逻辑在执行过程中往往需要保证数据访问的排他性,于是需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制。
Hibernate支持悲观锁和乐观锁两种锁机制。悲观锁,顾名思义,它悲观的认为在数据处理过程中一定存在修改数据的并发事务(包括本系统的其他事务或来自外部系统的事务),于是将处理的数据设置为锁定状态。悲观锁必须依赖数据库本身的锁机制才能真正保证数据访问的排他性。乐观锁,顾名思义,对并发事务持乐观态度(认为对数据的并发操作很少发生),通过更加宽松的锁机制解决悲观锁排他的数据访问对系统性能造成的严重影响。最常见的乐观锁是通过数据版本标识来实现的,读取数据时获得数据的版本号,更新数据时将此版本号加1,然后和数据库表对应记录的当前版本号进行比较,如果提交的数据版本号大于数据库中此记录的当前版本号则更新数据,否则认为是过期数据。
110、阐述实体对象的三种状态以及转换关系。
答:Hibernate中对象有三种状态:临时态(transient)、持久态(persistent)和游状态(detached),如下图所示。
图 Hibernate实体状态转换图
- 临时状态:当new一个实体对象后,这个对象处于临时状态,即这个对象只是一个保存临时数据的内存区域,如果没有变量引用这个对象,则会被JVM的垃圾回收机制回收。这个对象所保存的数据与数据库没有任何关系,除非通过Session的save或者saveOrUpdate把临时对象与数据库关联,并把数据插入或者更新到数据库,这个对象才转换为持久对象。
- 持久状态:持久化对象的实例在数据库中有对应的记录,并拥有一个持久化标识。对持久化对象进行delete操作后,数据库中对应的记录将被删除,那么持久化对象与数据库记录不再存在对应关系,持久化对象变成临时状态。持久化对象被修改变更后,不会马上同步到数据库,直到数据库事务提交。
- 游离状态:当Session进行了close、clear或者evict后,持久化对象虽然拥有持久化标识符和与数据库对应记录一致的值,但是因为会话已经消失,对象不在持久化管理之内,所以处于游离状态(也叫脱管状态)。游离状态的对象与临时状态对象是十分相似的,只是它还含有持久化标识。
111、如何理解Hibernate的延迟加载机制。在实际应用中,延迟加载与session关闭的矛盾是如何处理的?
答:延迟加载就是并不是在读取的时候就把数据加载进来,而是等到使用时再加载。Hibernate使用了虚拟代理机制实现延迟加载。返回给用户的并不是实体本身,而是实体对象的代理。代理对象在用户调用getter方法时就会去数据库加载数据。但加载数据就需要数据库连接。而当我们把会话关闭时,数据库连接就同时关闭了。
延迟加载与session关闭的矛盾一般可以这样处理:
① 关闭延迟加载特性。这种方式操作起来比较简单,因为hibernate的延迟加载特性是可以通过映射文件或者注解进行配置的,但这种解决方案存在明显的缺陷。首先,出现no session or session was closed就证明了系统中已经存在主外键关联,如果去掉延迟加载的话,则每次查询的开销都会变得很大。
②在session关闭之前先获取需要查询的数据(Hibernate.initialize()方法)。
③ 使用拦截器(Interceptor)或过滤器(Filter)控制Session。
112、举一个多对多关联的例子,并说明如何实现多对多关联映射。
答:例如:商品和订单、学生和课程都是典型的多对多关系。可以在实体类上通过@ManyToMany注解配置多对多关联或者通过映射文件中的<set>和<many-to-many>标签配置多对多关联,但是通常情况下,可以将多对多关联转换成两个多对一关联来实现多对多关联映射。
113、谈一下你对继承映射的理解。
答:继承关系的映射策略有三种:
①每个继承结构一张表(table per class hierarchy)
②每个子类一张表(table per subclass)
③ 每个具体类一张表(table per concrete class)
第一种方式属于单表策略,其优点在于查询子类对象的时候无需表连接,查询速度快,适合多态查询;缺点是可能导致表很大。后两种方式属于多表策略,其优点在于数据存储紧凑,其缺点是需要进行连接查询,不适合多态查询。
114、简述Hibernate常见优化策略。
答:
①制定合理的缓存策略
② 采用合理的Session管理机制
③ 尽量使用延迟加载特性
④设定合理的批处理参数
⑤ 如果可以, 选用UUID作为主键生成器
⑥如果可以, 选用基于version的乐观锁替代悲观锁
⑦ 在开发过程中, 开启hibernate.show_sql选项查看生成的SQL, 从而了解底层的状况;开发完成后关闭此选项
⑧ 数据库本身的优化(合理的索引, 缓存, 数据分区策略等)也会对持久层的性能带来可观的提升, 这些需要专业的DBA提供支持
115、谈一谈Hibernate的一级缓存、二级缓存和查询缓存。
答:Hibernate的Session提供了一级缓存的功能,默认总是有效的,当应用程序保存持久化实体、修改持久化实体时,Session并不会立即把这种改变提交到数据库,而是缓存在当前的Session中,除非显示调用了Session的flush()方法或通过close()方法关闭Session。通过一级缓存,可以减少程序与数据库的交互,从而提高数据库访问性能。
SessionFactory级别的二级缓存是全局性的,所有的Session可以共享这个二级缓存。不过二级缓存默认是关闭的,需要显示开启并指定需要使用哪种二级缓存实现类(可以使用第三方提供的实现)。一旦开启了二级缓存并设置了需要使用二级缓存的实体类,SessionFactory就会缓存访问过的该实体类的每个对象,除非缓存的数据超出了指定的缓存空间。
一级缓存和二级缓存都是对整个实体进行缓存,不会缓存普通属性,如果希望对普通属性进行缓存,可以使用查询缓存。查询缓存是将HQL或SQL语句以及它们的查询结果作为键值对进行缓存,对于同样的查询可以直接从缓存中获取数据。查询缓存默认也是关闭的,需要显示开启。
Java程序员面试题集(116-135)
摘要:这一部分讲解基于Java的Web开发相关面试题,即便在Java走向没落的当下,基于Java的Web开发因为拥有非常成熟的解决方案,仍然被广泛应用。不管你的Web开发中是否使用框架,JSP和Servlet都是一个必备的基础,在面试的时候被问到的概率还是很高的。
116、说出Servlet的生命周期,并说出Servlet和CGI的区别?
答:Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其init()方法进行Servlet的初始化;请求到达时调用Servlet的service方法,service方法会调用与请求对应的doGet或doPost等方法;当服务器关闭会项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的destroy方法。Servlet与CGI的区别在于Servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI 对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于Servlet。
【补充1】SUN公司在1996年发布Servlet技术就是为了和CGI进行竞争,Servlet是一个特殊的Java程序,一个基于Java的Web应用通常包含一个或多个Servlet类。 Servlet不能够自行创建并执行,它是在Servlet容器中运行的,容器将用户的请求传递给Servlet程序,此外将Servlet的响应回传给用户。通常一个Servlet会关联一个或多个JSP页面。以前CGI经常因为性能开销上的问题被诟病,然而Fast CGI早就已经解决了CGI效率上的问题,所以面试的时候大可不必诟病CGI,腾讯的网站就使用了CGI技术,相信你也没感觉它哪里不好。
【补充2】Servlet接口定义了5个方法,其中前三个方法与Servlet生命周期相关:
- void init(ServletConfig config) throws ServletException
- void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException
- void destory()
- java.lang.String getServletInfo()
- ServletConfig getServletConfig()
117、转发(forward)和重定向(redirect)的区别?
答:forward是容器中控制权的转向,是服务器请求资源,服务器直接访问目标地址的URL,把那个URL 的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容是从哪儿来的,所以它的地址栏中还是原来的地址。redirect就是服务器端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,因此从浏览器的地址栏中可以看到跳转后的链接地址。前者更加高效,在前者可以满足需要时,尽量使用转发(通过RequestDispatcher对象的forward方法,RequestDispatcher对象可以通过ServletRequest对象的getRequestDispatcher方法获得),并且,这样也有助于隐藏实际的链接;在有些情况下,比如,需要跳转到一个其它服务器上的资源,则必须使用重定向(通过HttpServletResponse对象调用其sendRedirect方法)。
118、JSP有哪些内置对象?作用分别是什么?
答:JSP有9个内置对象:
- request:封装客户端的请求,其中包含来自GET或POST请求的参数;
- response:封装服务器对客户端的响应;
- pageContext:通过该对象可以获取其他对象;
- session:封装用户会话的对象;
- application:封装服务器运行环境的对象;
- out:输出服务器响应的输出流对象;
- config:Web应用的配置对象;
- page:JSP页面本身(相当于Java程序中的this);
- exception:封装页面抛出异常的对象。
【补充】如果用Servlet来生成网页中的动态内容无疑是非常繁琐的工作,另一方面,所有的文本和HTML标签都是硬编码,即使做出微小的修改,都需要进行重新编译。JSP解决了Servlet的这些问题,它是Servlet很好的补充,可以专门用作呈现给用户的视图(View),而Servlet作为控制器(Controller)专门负责处理用户请求并转发或重定向到某个页面。基于Java的Web开发很多都同时使用了Servlet和JSP。JSP页面其实是一个Servlet,能够运行Servlet的服务器(Servlet容器)通常也是JSP容器,可以提供JSP页面的运行环境,Tomcat就是一个Servlet/JSP容器。第一次请求一个JSP页面时,Servlet/JSP容器首先将JSP页面转换成一个JSP页面的实现类,这是一个实现了JspPage接口或其子接口HttpJspPage的Java类。JspPage接口是Servlet的子接口,因此每个JSP页面都是一个Servlet。转换成功后,容器会编译Servlet类,之后容器加载和实例化Java字节码,并执行它通常对Servlet所做的生命周期操作。对同一个JSP页面的后续请求,容器会查看这个JSP页面是否被修改过,如果修改过就会重新转换并重新编译并执行。如果没有则执行内存中已经存在的Servlet实例。我们可以看一段JSP代码对应的Java程序就知道一切了,而且9个内置对象的神秘面纱也会被揭开。
JSP页面:
- <%@ page pageEncoding="UTF-8"%>
- <%
- String path = request.getContextPath();
- String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
- %>
-
- <!DOCTYPE html>
- <html>
- <head>
- <base href="<%=basePath%>">
- <title>首页</title>
- <style type="text/css">
- * { font-family: "Arial"; }
- </style>
- </head>
-
- <body>
- <h1>Hello, World!</h1>
- <hr/>
- <h2>Current time is: <%= new java.util.Date().toString() %></h2>
- </body>
- </html>
对应的Java代码:
-
-
-
-
-
-
-
-
- package org.apache.jsp;
-
- import javax.servlet.*;
- import javax.servlet.http.*;
- import javax.servlet.jsp.*;
-
- public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
- implements org.apache.jasper.runtime.JspSourceDependent {
-
- private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory
- .getDefaultFactory();
-
- private static java.util.Map<java.lang.String, java.lang.Long> _jspx_dependants;
-
- private javax.el.ExpressionFactory _el_expressionfactory;
- private org.apache.tomcat.InstanceManager _jsp_instancemanager;
-
- public java.util.Map<java.lang.String, java.lang.Long> getDependants() {
- return _jspx_dependants;
- }
-
- public void _jspInit() {
- _el_expressionfactory = _jspxFactory.getJspApplicationContext(
- getServletConfig().getServletContext()).getExpressionFactory();
- _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory
- .getInstanceManager(getServletConfig());
- }
-
- public void _jspDestroy() {
- }
-
- public void _jspService(
- final javax.servlet.http.HttpServletRequest request,
- final javax.servlet.http.HttpServletResponse response)
- throws java.io.IOException, javax.servlet.ServletException {
-
- final javax.servlet.jsp.PageContext pageContext;
- javax.servlet.http.HttpSession session = null;
- final javax.servlet.ServletContext application;
- final javax.servlet.ServletConfig config;
- javax.servlet.jsp.JspWriter out = null;
- final java.lang.Object page = this;
- javax.servlet.jsp.JspWriter _jspx_out = null;
- javax.servlet.jsp.PageContext _jspx_page_context = null;
-
- try {
- response.setContentType("text/html;charset=UTF-8");
- pageContext = _jspxFactory.getPageContext(this, request, response,
- null, true, 8192, true);
- _jspx_page_context = pageContext;
- application = pageContext.getServletContext();
- config = pageContext.getServletConfig();
- session = pageContext.getSession();
- out = pageContext.getOut();
- _jspx_out = out;
-
- out.write('\r');
- out.write('\n');
-
- String path = request.getContextPath();
- String basePath = request.getScheme() + "://"
- + request.getServerName() + ":" + request.getServerPort()
- + path + "/";
-
- out.write("\r\n");
- out.write("\r\n");
- out.write("<!DOCTYPE html>\r\n");
- out.write("<html>\r\n");
- out.write(" <head>\r\n");
- out.write(" <base href=\"");
- out.print(basePath);
- out.write("\">\r\n");
- out.write(" <title>首页</title>\r\n");
- out.write(" <style type=\"text/css\">\r\n");
- out.write(" \t* { font-family: \"Arial\"; }\r\n");
- out.write(" </style>\r\n");
- out.write(" </head>\r\n");
- out.write(" \r\n");
- out.write(" <body>\r\n");
- out.write(" <h1>Hello, World!</h1>\r\n");
- out.write(" <hr/>\r\n");
- out.write(" <h2>Current time is: ");
- out.print(new java.util.Date().toString());
- out.write("</h2>\r\n");
- out.write(" </body>\r\n");
- out.write("</html>\r\n");
- } catch (java.lang.Throwable t) {
- if (!(t instanceof javax.servlet.jsp.SkipPageException)) {
- out = _jspx_out;
- if (out != null && out.getBufferSize() != 0)
- try {
- out.clearBuffer();
- } catch (java.io.IOException e) {
- }
- if (_jspx_page_context != null)
- _jspx_page_context.handlePageException(t);
- else
- throw new ServletException(t);
- }
- } finally {
- _jspxFactory.releasePageContext(_jspx_page_context);
- }
- }
- }
119、get和post请求的区别?
答:
①get请求用来从服务器上获得资源,而post是用来向服务器提交数据;
②get将表单中数据按照name=value的形式,添加到action 所指向的URL 后面,并且两者使用“?”连接,而各个变量之间使用“&”连接;post是将表单中的数据放在HTML头部(header),传递到action所指向URL;
③get传输的数据要受到URL长度限制(1024字节);而post可以传输大量的数据,上传文件只能使用post方式;
④使用get时参数会显示在地址栏上,如果这些数据不是敏感数据,那么可以使用get;对于敏感数据还是应用使用post;
⑤get使用MIME类型application/x-www-form-urlencoded的URL 编码(URL encoding,也叫百分号编码)文本的格式传递参数,保证被传送的参数由遵循规范的文本组成,例如一个空格的编码是"%20"。
120、常用的Web容器
答:Unix和Linux平台下使用最广泛的免费HTTP服务器是Apache服务器,而Windows平台的服务器通常使用IIS作为Web服务器。选择Web服务器应考虑的因素有:性能、安全性、日志和统计、虚拟主机、代理服务器、缓冲服务和集成应用程序等。下面是对常用服务器的简介:
- IIS:Microsoft的Web服务器产品为Internet Information Services。IIS 是允许在公共Intranet或Internet上发布信息的Web服务器。IIS是目前最流行的Web服务器产品之一,很多著名的网站都是建立在IIS的平台上。IIS提供了一个图形界面的管理工具,称为Internet服务管理器,可用于监视配置和控制Internet服务。IIS是一种Web服务组件,其中包括Web服务器、FTP服务器、NNTP服务器和SMTP服务器,分别用于网页浏览、文件传输、新闻服务和邮件发送等方面,它使得在网络(包括互联网和局域网)上发布信息成了一件很容易的事。它提供ISAPI(Intranet Server API)作为扩展Web服务器功能的编程接口;同时,它还提供一个Internet数据库连接器,可以实现对数据库的查询和更新。
- Kangle:Kangle Web服务器是一款跨平台、功能强大、安全稳定、易操作的高性能Web服务器和反向代理服务器软件。此外,Kangle也是一款专为做虚拟主机研发的Web服务器。实现虚拟主机独立进程、独立身份运行。用户之间安全隔离,一个用户出问题不影响其他用户。支持PHP、ASP、ASP.NET、Java、Ruby等多种动态开发语言。
- WebSphere:WebSphere Application Server是功能完善、开放的Web应用程序服务器,是IBM电子商务计划的核心部分,它是基于Java的应用环境,用于建立、部署和管理Internet和Intranet Web应用程序,适应各种Web应用程序服务器的需要,范围从简单到高级直到企业级。
- WebLogic:BEA WebLogic Server是一种多功能、基于标准的Web应用服务器,为企业构建自己的应用提供了坚实的基础。各种应用开发、部署所有关键性的任务,无论是集成各种系统和数据库,还是提交服务、跨Internet协作,Weblogic都提供了相应的支持。由于它具有全面的功能、对开放标准的遵从性、多层架构、支持基于组件的开发,基于Internet的企业都选择它来开发、部署最佳的应用。BEA WebLogic Server在使应用服务器成为企业应用架构的基础方面一直处于领先地位,为构建集成化的企业级应用提供了稳固的基础,它们以 Internet的容量和速度,在连网的企业之间共享信息、提交服务,实现协作自动化。
- Apache:目前Apache仍然是世界上用得最多的Web服务器,市场占有率约为60%左右。世界上很多著名的网站都是Apache的产物,它的成功之处主要在于它的源代码开放、有一支强大的开发团队、支持跨平台的应用(可以运行在几乎所有的Unix、Windows、Linux系统平台上)以及它的可移植性等方面。
- Tomcat:Tomcat是一个开放源代码、运行Servlet和JSP的容器。TomcatServer实现了Servlet和JSP规范。此外,Tomcat还实现了Apache-Jakarta规范而且比绝大多数商业应用软件服务器要好,因此目前也有不少的Web服务器都选择了Tomcat。
- Nginx:读作"engine x",是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP代理服务器。 Nginx是由Igor Sysoev为俄罗斯访问量第二的 Rambler.ru站点开发的,第一个公开版本0.1.0发布于2004年10月4日。其将源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。
121、JSP 和Servlet 有有什么关系?
答:其实这个问题在上面已经阐述过了,Servlet是一个特殊的Java程序,它运行于服务器的JVM中,能够依靠服务器的支持向浏览器提供显示内容。JSP本质上是Servlet的一种简易形式, JSP会被服务器处理成一个类似于Servlet的Java程序,可以简化页面内容的生成。Servlet和JSP最主要的不同点在于,Servlet 的应用逻辑是在Java 文件中,并且完全从表示层中的HTML分离开来。而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp 的文件(有人说,Servlet就是在Java中写HTML,而JSP就是在HTML中写Java代码,当然,这个说法还是很片面的)。JSP侧重于视图,Servlet更侧重于控制逻辑,在MVC架构模式中,JSP适合充当视图(view)而Servlet适合充当控制器(controller)。
122、JSP中的四种作用域?
答:page、request、session和application,具体如下:
①page 代表与一个页面相关的对象和属性。
②request 代表与Web客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个Web 组件;需要在页面显示的临时数据可以置于此作用域
③session代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中
④application代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。
123、如何实现JSP或Servlet的单线程模式?
答:
- <%@page isThreadSafe=”false”%>
【补充】Servlet默认的工作模式是单实例多线程,如果Servlet实现了标识接口SingleThreadModel又或是JSP页面通过page指令设置isThreadSafe属性为false,那么它们生成的Java代码会以单线程多实例方式工作。显然,这样做会导致每个请求创建一个Servlet实例,这种实践将导致严重的性能问题。
124、实现会话跟踪的技术有哪些?
答:由于HTTP协议本身是无状态的,服务器为了区分不同的用户,就需要对用户会话进行跟踪,简单的说就是为用户进行登记,为用户分配唯一的ID,下一次用户在请求中包含此ID,服务器据此判断到底是哪一个用户。
①URL 重写:在URL中添加用户会话的信息作为请求的参数,或者将唯一的会话ID添加到URL结尾以标识一个会话。
②设置表单隐藏域:将和会话跟踪相关的字段添加到隐式表单域中,这些信息不会在浏览器中显示但是提交表单时会提交给服务器。
这两种方式很难处理跨越多个页面的信息传递,因为如果每次都要修改URL或在页面中添加隐式表单域来存储用户会话相关信息,事情将变得非常麻烦。
③cookie:cookie有两种,一种是基于窗口的,浏览器窗口关闭后,cookie就没有了;另一种是将信息存储在一个临时文件中,并设置存在的时间。当用户通过浏览器和服务器建立一次会话后,会话ID就会随响应信息返回存储在基于窗口的cookie中,那就意味着只要浏览器没有关闭,会话没有超时,下一次请求时这个会话ID又会提交给服务器让服务器识别用户身份。会话中可以为用户保存信息。会话对象是在服务器内存中的,而基于窗口的cookie是在客户端内存中的。如果浏览器禁用了cookie,那么就需要通过下面两种方式进行会话跟踪。当然,在使用cookie时要注意几点:首先不要在cookie中存放敏感信息;其次cookie存储的数据量有限(4k),不能将过多的内容存储cookie中;再者浏览器通常只允许一个站点最多存放20个cookie。当然,和用户会话相关的其他信息(除了会话ID)也可以存在cookie方便进行会话跟踪。
④HttpSession:在所有会话跟踪技术中,HttpSession对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建HttpSession,每个用户可以访问他自己的HttpSession。可以通过HttpServletRequest对象的getSession方法获得HttpSession,通过HttpSession的setAttribute方法可以将一个值放在HttpSession中,通过调用HttpSession对象的getAttribute方法,同时传入属性名就可以获取保存在HttpSession中的对象。与上面三种方式不同的是,HttpSession放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的Servlet容器可以在内存将满时将HttpSession中的对象移到其他存储设备中,但是这样势必影响性能。添加到HttpSession中的值可以是任意Java对象,这个对象最好实现了Serializable接口,这样Servlet容器在必要的时候可以将其序列化到文件中,否则在序列化时就会出现异常。
126、过滤器有哪些作用和用法?
答: Java Web开发中的过滤器(filter)是从Servlet 2.3规范开始增加的功能,并在Servlet 2.4规范中得到增强。对Web应用来说,过滤器是一个驻留在服务器端的Web组件,它可以截取客户端和服务器之间的请求与响应信息,并对这些信息进行过滤。当Web容器接受到一个对资源的请求时,它将判断是否有过滤器与这个资源相关联。如果有,那么容器将把请求交给过滤器进行处理。在过滤器中,你可以改变请求的内容,或者重新设置请求的报头信息,然后再将请求发送给目标资源。当目标资源对请求作出响应时候,容器同样会将响应先转发给过滤器,再过滤器中,你可以对响应的内容进行转换,然后再将响应发送到客户端。
常见的过滤器用途主要包括:对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、转换图象格式、对响应内容进行压缩以减少传输量、对请求或响应进行加解密处理、触发资源访问事件、对XML的输出应用XSLT等。
和过滤器相关的接口主要有:Filter、FilterConfig、FilterChain
编码过滤器的例子:
- package com.lovo.filter;
-
- import java.io.IOException;
-
- import javax.servlet.Filter;
- import javax.servlet.FilterChain;
- import javax.servlet.FilterConfig;
- import javax.servlet.ServletException;
- import javax.servlet.ServletRequest;
- import javax.servlet.ServletResponse;
- import javax.servlet.annotation.WebFilter;
- import javax.servlet.annotation.WebInitParam;
-
- @WebFilter(urlPatterns = { "*" },
- initParams = {@WebInitParam(name="encoding", value="utf-8")})
- public class CodingFilter implements Filter {
- private String defaultEncoding = "utf-8";
-
- @Override
- public void destroy() {
- }
-
- @Override
- public void doFilter(ServletRequest req, ServletResponse resp,
- FilterChain chain) throws IOException, ServletException {
- req.setCharacterEncoding(defaultEncoding);
- resp.setCharacterEncoding(defaultEncoding);
- chain.doFilter(req, resp);
- }
-
- @Override
- public void init(FilterConfig config) throws ServletException {
- String encoding = config.getInitParameter("encoding");
- if (encoding != null) {
- defaultEncoding = encoding;
- }
- }
- }
下载计数过滤器:
- package com.accp.filter;
-
- import java.io.File;
- import java.io.FileReader;
- import java.io.FileWriter;
- import java.io.IOException;
- import java.util.Properties;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- import javax.servlet.Filter;
- import javax.servlet.FilterChain;
- import javax.servlet.FilterConfig;
- import javax.servlet.ServletException;
- import javax.servlet.ServletRequest;
- import javax.servlet.ServletResponse;
- import javax.servlet.annotation.WebFilter;
- import javax.servlet.http.HttpServletRequest;
-
- @WebFilter(urlPatterns = {"/*"})
- public class DownloadCounterFilter implements Filter {
-
- private ExecutorService executorService = Executors.newSingleThreadExecutor();
- private Properties downloadLog;
- private File logFile;
-
- @Override
- public void destroy() {
- executorService.shutdown();
- }
-
- @Override
- public void doFilter(ServletRequest req, ServletResponse resp,
- FilterChain chain) throws IOException, ServletException {
- HttpServletRequest request = (HttpServletRequest) req;
- final String uri = request.getRequestURI();
- executorService.execute(new Runnable() {
-
- @Override
- public void run() {
- String value = downloadLog.getProperty(uri);
- if(value == null) {
- downloadLog.setProperty(uri, "1");
- }
- else {
- int count = Integer.parseInt(value);
- downloadLog.setProperty(uri, String.valueOf(++count));
- }
- try {
- downloadLog.store(new FileWriter(logFile), "");
- }
- catch (IOException e) {
- e.printStackTrace();
- }
- }
- });
- chain.doFilter(req, resp);
- }
-
- @Override
- public void init(FilterConfig config) throws ServletException {
- String appPath = config.getServletContext().getRealPath("/");
- logFile = new File(appPath, "downloadLog.txt");
- if(!logFile.exists()) {
- try {
- logFile.createNewFile();
- }
- catch(IOException e) {
- e.printStackTrace();
- }
- }
- downloadLog = new Properties();
- try {
- downloadLog.load(new FileReader(logFile));
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- }
127、监听器有哪些作用和用法?
答:Java Web开发中的监听器(listener)就是application、session、request三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件,如下所示:
①ServletContextListener:对Servlet上下文的创建和销毁进行监听。
②ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换。
③HttpSessionListener:对Session的创建和销毁进行监听。
补充:session的销毁有两种情况:1session超时(可以在web.xml中通过<session-config>/<session-timeout>标签配置超时时间);2通过调用session对象的invalidate()方法使session失效。
④HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听。
⑤ServletRequestListener:对请求对象的初始化和销毁进行监听。
⑥ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。
下面是一个统计网站最多在线人数及时间的监听器:
上下文监听器,在上下文中放置onLineCount和maxOnLineCount属性,初始值为0
- package com.lovo.listeners;
-
- import javax.servlet.ServletContextEvent;
- import javax.servlet.ServletContextListener;
- import javax.servlet.annotation.WebListener;
-
- @WebListener
- public class InitListener implements ServletContextListener {
-
- @Override
- public void contextDestroyed(ServletContextEvent evt) {
- }
-
- @Override
- public void contextInitialized(ServletContextEvent evt) {
- evt.getServletContext().setAttribute("onLineCount", 0);
- evt.getServletContext().setAttribute("maxOnLineCount", 0);
- }
-
- }
会话监听器,监听到会话创建时判断是否达到最大在线人数并记录时间
- package com.lovo.listeners;
-
- import java.text.DateFormat;
- import java.text.SimpleDateFormat;
- import java.util.Date;
-
- import javax.servlet.ServletContext;
- import javax.servlet.annotation.WebListener;
- import javax.servlet.http.HttpSessionEvent;
- import javax.servlet.http.HttpSessionListener;
-
- @WebListener
- public class MaxCountListener implements HttpSessionListener {
-
- @Override
- public void sessionCreated(HttpSessionEvent event) {
- ServletContext ctx = event.getSession().getServletContext();
- int count = Integer.parseInt(ctx.getAttribute("onLineCount").toString());
- count++;
- ctx.setAttribute("onLineCount", count);
- int maxOnLineCount = Integer.parseInt(ctx.getAttribute("maxOnLineCount").toString());
- if (count > maxOnLineCount) {
- ctx.setAttribute("maxOnLineCount", count);
- DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- ctx.setAttribute("date", df.format(new Date()));
- }
- }
-
- @Override
- public void sessionDestroyed(HttpSessionEvent event) {
- ServletContext app = event.getSession().getServletContext();
- int count = Integer.parseInt(app.getAttribute("onLineCount").toString());
- count--;
- app.setAttribute("onLineCount", count);
- }
- }
【注意】这里使用注解@WebListener配置该监听器,当然你可以在web.xml文件中用<listener>标签配置监听器,如下题所示。
128、web.xml 的作用?
答:用于配置Web应用的相关信息,如:监听器(listener)、过滤器(filter)、 Servlet、相关参数、会话超时时间、安全验证方式、错误页面等。例如:
①配置spring上下文加载监听器加载Spring配置文件:
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>classpath:applicationContext.xml</param-value>
- </context-param>
-
- <listener>
- <listener-class>
- org.springframework.web.context.ContextLoaderListener
- </listener-class>
- </listener>
②配置Spring的OpenSessionInView过滤器来解决延迟加载和Hibernate会话关闭的矛盾:
- <filter>
- <filter-name>openSessionInView</filter-name>
- <filter-class>
- org.springframework.orm.hibernate3.support.OpenSessionInViewFilter
- </filter-class>
- </filter>
-
- <filter-mapping>
- <filter-name>openSessionInView</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
③配置会话超时时间为10分钟:
- <session-config>
- <session-timeout>10</session-timeout>
- </session-config>
④配置404和Exception的错误页面:
- <error-page>
- <error-code>404</error-code>
- <location>/error.jsp</location>
- </error-page>
-
- <error-page>
- <exception-type>java.lang.Exception</exception-type>
- <location>/error.jsp</location>
- </error-page>
⑤配置安全认证方式:
- <security-constraint>
- <web-resource-collection>
- <web-resource-name>ProtectedArea</web-resource-name>
- <url-pattern>/admin/*</url-pattern>
- <http-method>GET</http-method>
- <http-method>POST</http-method>
- </web-resource-collection>
- <auth-constraint>
- <role-name>admin</role-name>
- </auth-constraint>
- </security-constraint>
-
- <login-config>
- <auth-method>BASIC</auth-method>
- </login-config>
-
- <security-role>
- <role-name>admin</role-name>
- </security-role>
【补充1】从Servlet 3开始,可以不用在web.xml中部署Servlet(小服务)、Filter(过滤器)、Listener(监听器)等Web组件,Servlet 3提供了基于注解的部署方式,可以分别使用@WebServlet、@WebFilter、@WebListener三个部署小服务、过滤器、监听器。
【补充2】如果Web提供了有价值的商业信息或者是敏感数据,那么站点的安全性就是必须考虑的问题。安全认证是实现安全性的重要手段,认证就是要解决“Are you who you say you are?”的问题。认证的方式非常多,简单说来可以分为三类:
A.What you know? --- 口令
B.What you have? --- 数字证书(U盾、密保卡)
C.Who you are? --- 指纹识别、虹膜识别
在Tomcat中可以通过建立安全套接字层(Secure Socket Layer, SSL)以及通过基本验证或表单验证来实现对安全性的支持。
129、你的项目中使用过哪些JSTL标签?
答:项目中主要使用了JSTL的核心标签库,包括<c:if>、<c:choose>、<c: when>、<c: otherwise>、<c:forEach>等,主要用于构造循环和分支结构以控制显示逻辑。
【说明】虽然JSTL标签库提供了core、sql、fmt、xml等标签库,但是实际开发中建议只使用核心标签库(core),而且最好只使用分支和循环标签并辅以表达式语言(EL),这样才能真正做到数据显示和业务逻辑的分离,这才是最佳实践。
130、使用标签库有什么好处?如何自定义JSP标签?
答:使用标签库的好处包括以下几个方面:
- 分离JSP页面的内容和逻辑,简化了Web开发;
- 开发者可以创建自定义标签来封装业务逻辑和显示逻辑;
- 标签具有很好的可移植性、可维护性和可重用性;
- 避免了对Scriptlet(小脚本)的使用(很多公司的项目开发都不允许在JSP中书写小脚本)
自定义JSP标签包括以下几个步骤:
- 编写一个Java类实现实现Tag/BodyTag/IterationTag接口(通常不直接实现这些接口而是继承TagSupport/BodyTagSupport/SimpleTagSupport类,这是对适配器模式中缺省适配模式的应用)
- 重写doStartTag()、doEndTag()等方法,定义标签要完成的功能
- 编写扩展名为tld的标签描述文件对自定义标签进行部署,tld文件通常放在WEB-INF文件夹或其子目录
- 在JSP页面中使用taglib指令引用该标签库
下面是一个例子:
标签类源代码
- package com.lovo.tags;
-
- import java.io.IOException;
- import java.text.SimpleDateFormat;
- import java.util.Date;
-
- import javax.servlet.jsp.JspException;
- import javax.servlet.jsp.JspWriter;
- import javax.servlet.jsp.tagext.TagSupport;
-
- public class TimeTag extends TagSupport {
- private static final long serialVersionUID = 1L;
-
- private String format = "yyyy-MM-dd hh:mm:ss";
- private String foreColor = "black";
- private String backColor = "white";
-
- public int doStartTag() throws JspException {
- SimpleDateFormat sdf = new SimpleDateFormat(format);
- JspWriter writer = pageContext.getOut();
- StringBuilder sb = new StringBuilder();
- sb.append(String.format("<span style='color:%s;background-color:%s'>%s</span>",
- foreColor, backColor, sdf.format(new Date())));
- try {
- writer.print(sb.toString());
- } catch(IOException e) {
- e.printStackTrace();
- }
- return SKIP_BODY;
- }
-
- public void setFormat(String format) {
- this.format = format;
- }
-
- public void setForeColor(String foreColor) {
- this.foreColor = foreColor;
- }
-
- public void setBackColor(String backColor) {
- this.backColor = backColor;
- }
- }
标签库描述文件(该文件通常放在WEB-INF目录或其子目录下)
- <?xml version="1.0" encoding="UTF-8" ?>
- <taglib xmlns="http://java.sun.com/xml/ns/j2ee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
- http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
- version="2.0">
-
- <description>定义标签库</description>
- <tlib-version>1.0</tlib-version>
- <short-name>MyTag</short-name>
- <tag>
- <name>time</name>
- <tag-class>com.lovo.tags.TimeTag</tag-class>
- <body-content>empty</body-content>
- <attribute>
- <name>format</name>
- <required>false</required>
- </attribute>
- <attribute>
- <name>foreColor</name>
- </attribute>
- <attribute>
- <name>backColor</name>
- </attribute>
- </tag>
- </taglib>
JSP页面
- <%@ page pageEncoding="UTF-8"%>
- <%@ taglib prefix="my" uri="/WEB-INF/tld/my.tld" %>
- <%
- String path = request.getContextPath();
- String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
- %>
-
- <!DOCTYPE html>
- <html>
- <head>
- <base href="<%=basePath%>">
- <title>首页</title>
- <style type="text/css">
- * { font-family: "Arial"; font-size:72px; }
- </style>
- </head>
-
- <body>
- <my:time format="yyyy-MM-dd" backColor="blue" foreColor="yellow"/>
- </body>
- </html>
运行结果
【注意】如果要将自定义的标签库发布成JAR文件,需要将标签库描述文件(tld文件)放在JAR文件的META-INF目录下,可以JDK自带的jar工具完成JAR文件的生成,如果不清楚如何操作,可以请教谷老师(各位亲,点击这里真的可以使用用谷歌)和百老师。
131、表达式语言(EL)的隐式对象及其作用?
答:pageContext、initParam(访问上下文参数)、param(访问请求参数)、paramValues、header(访问请求头)、headerValues、cookie(访问cookie)、applicationScope(访问application作用域)、sessionScope(访问session作用域)、requestScope(访问request作用域)、pageScope(访问page作用域)。用法如下所示:
${pageContext.request.method}
${pageContext["request"]["method"]}
${pageContext.request["method"]}
${pageContext["request"].method}
${initParam.defaultEncoding}
${header["accept-language"]}
${headerValues["accept-language"][0]}
${cookie.jsessionid.value}
${sessionScope.loginUser.username}
【补充】表达式语言的.和[]运算作用是一致的,唯一的差别在于如果访问的属性名不符合Java标识符命名规则,例如上面的accept-language就不是一个有效的Java标识符,那么这时候就只能用[]运算符而不能使用.获取它的值
132、表达式语言(EL)支持哪些运算符?
答:除了.和[]运算符,EL还提供了:
- 算术运算符:+、-、*、/或div、%或mod
- 关系运算符:==或eq、!=或ne、>或gt、>=或ge、<或lt、<=或le
- 逻辑运算符:&&或and、||或or、!或not
- 条件运算符:${statement? A : B}(跟Java的条件运算符类似)
- empty运算符:检查一个值是否为null或者空(数组长度为0或集合中没有元素也返回true)
133、Java Web开发的Model 1和Model 2分别指的是什么?
答:Model 1是以页面为中心的Java Web开发,只适合非常小型的应用程序,Model 2是基于MVC架构模式的应用,这一点在前文的面试题中已经详细讲解过了。
134、Servlet 3中的异步处理指的是什么?
答:在Servlet 3中引入了一项新的技术可以让Servlet异步处理请求。有人可能会质疑,既然都有多线程了,还需要异步处理请求吗?答案是肯定的,因为如果一个任务处理时间相当长,那么Servlet或Filter会一直占用着请求处理线程直到任务结束,随着并发用户的增加,容器将会遭遇线程超出的风险,这这种情况下很多的请求将会被堆积起来而后续的请求可能会遭遇拒绝服务,直到有资源可以处理请求为止。异步特性可以帮助应用节省容器中的线程,特别适合执行时间长而且用户需要得到结果的任务,如果用户不需要得到结果则直接将一个Runnable对象交给Executor(如果不清楚请查看前文关于多线程和线程池的部分)并立即返回即可。
【补充】多线程在Java诞生初期无疑是一个亮点,而Servlet单实例多线程的工作方式也曾为其赢得美名,然而技术的发展往往会颠覆我们很多的认知,就如同当年爱因斯坦的相对论颠覆了牛顿的经典力学一般。事实上,异步处理绝不是Serlvet 3首创,如果你了解Node.js的话,对Servlet 3的这个重要改进就不以为奇了。
下面是一个支持异步处理请求的Servlet的例子:
- package com.lovo.servlet;
-
- import java.io.IOException;
-
- import javax.servlet.AsyncContext;
- import javax.servlet.ServletException;
- import javax.servlet.annotation.WebServlet;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
-
- @WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
- public class AsyncServlet extends HttpServlet {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void doGet(HttpServletRequest req, HttpServletResponse resp)
- throws ServletException, IOException {
-
- req.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
-
- final AsyncContext ctx = req.startAsync();
-
- ctx.start(new Runnable() {
-
- @Override
- public void run() {
-
-
- ctx.complete();
- }
- });
- }
- }
135、如何在基于Java的Web项目中实现文件上传和下载?
答:(稍后呈现,我准备用HTML5写一个带进度条的客户端,然后再用Servlet 3提供的文件上传支持来做一个多文件上传的例子)
Java程序员面试题集(136-150)
摘要:这一部分主要是数据结构和算法相关的面试题目,虽然只有15道题目,但是包含的信息量还是很大的,很多题目背后的解题思路和算法是非常值得玩味的。
136、给出下面的二叉树先序、中序、后序遍历的序列?
答:先序序列:ABDEGHCF;中序序列:DBGEHACF;后序序列:DGHEBFCA。
补充:二叉树也称为二分树,它是树形结构的一种,其特点是每个结点至多有二棵子树,并且二叉树的子树有左右之分,其次序不能任意颠倒。二叉树的遍历序列按照访问根节点的顺序分为先序(先访问根节点,接下来先序访问左子树,再先序访问右子树)、中序(先中序访问左子树,然后访问根节点,最后中序访问右子树)和后序(先后序访问左子树,再后序访问右子树,最后访问根节点)。如果知道一棵二叉树的先序和中序序列或者中序和后序序列,那么也可以还原出该二叉树。
例如,已知二叉树的先序序列为:xefdzmhqsk,中序序列为:fezdmxqhks,那么还原出该二叉树应该如下图所示:
137、你知道的排序算法都哪些?用Java写一个排序系统。
答:稳定的排序算法有:插入排序、选择排序、冒泡排序、鸡尾酒排序、归并排序、二叉树排序、基数排序等;不稳定排序算法包括:希尔排序、堆排序、快速排序等。
下面是关于排序算法的一个列表:
下面按照策略模式给出一个排序系统,实现了冒泡、归并和快速排序。
Sorter.java
- package com.jackfrued.util;
-
- import java.util.Comparator;
-
-
-
-
-
-
- public interface Sorter {
-
-
-
-
-
- public <T extends Comparable<T>> void sort(T[] list);
-
-
-
-
-
-
- public <T> void sort(T[] list, Comparator<T> comp);
- }
BubbleSorter.java
- package com.jackfrued.util;
-
- import java.util.Comparator;
-
-
-
-
-
-
- public class BubbleSorter implements Sorter {
-
- @Override
- public <T extends Comparable<T>> void sort(T[] list) {
- boolean swapped = true;
- for(int i = 1; i < list.length && swapped;i++) {
- swapped= false;
- for(int j = 0; j < list.length - i; j++) {
- if(list[j].compareTo(list[j+ 1]) > 0 ) {
- T temp = list[j];
- list[j]= list[j + 1];
- list[j+ 1] = temp;
- swapped= true;
- }
- }
- }
- }
-
- @Override
- public <T> void sort(T[] list,Comparator<T> comp) {
- boolean swapped = true;
- for(int i = 1; i < list.length && swapped; i++) {
- swapped = false;
- for(int j = 0; j < list.length - i; j++) {
- if(comp.compare(list[j], list[j + 1]) > 0 ) {
- T temp = list[j];
- list[j]= list[j + 1];
- list[j+ 1] = temp;
- swapped= true;
- }
- }
- }
- }
- }
MergeSorter.java
- package com.jackfrued.util;
-
- import java.util.Comparator;
-
-
-
-
-
-
-
-
-
-
- public class MergeSorter implements Sorter {
-
- @Override
- public <T extends Comparable<T>> void sort(T[] list) {
- T[] temp = (T[]) new Comparable[list.length];
- mSort(list,temp, 0, list.length- 1);
- }
-
- private <T extends Comparable<T>> void mSort(T[] list, T[] temp, int low, int high) {
- if(low == high) {
- return ;
- }
- else {
- int mid = low + ((high -low) >> 1);
- mSort(list,temp, low, mid);
- mSort(list,temp, mid + 1, high);
- merge(list,temp, low, mid + 1, high);
- }
- }
-
- private <T extends Comparable<T>> void merge(T[] list, T[] temp, int left, int right, int last) {
- int j = 0;
- int lowIndex = left;
- int mid = right - 1;
- int n = last - lowIndex + 1;
- while (left <= mid && right <= last){
- if (list[left].compareTo(list[right]) < 0){
- temp[j++] = list[left++];
- } else {
- temp[j++] = list[right++];
- }
- }
- while (left <= mid) {
- temp[j++] = list[left++];
- }
- while (right <= last) {
- temp[j++] = list[right++];
- }
- for (j = 0; j < n; j++) {
- list[lowIndex + j] = temp[j];
- }
- }
-
- @Override
- public <T> void sort(T[] list, Comparator<T> comp) {
- T[]temp = (T[])new Comparable[list.length];
- mSort(list,temp, 0, list.length- 1, comp);
- }
-
- private <T> void mSort(T[] list, T[] temp, int low, int high, Comparator<T> comp) {
- if(low == high) {
- return ;
- }
- else {
- int mid = low + ((high -low) >> 1);
- mSort(list,temp, low, mid, comp);
- mSort(list,temp, mid + 1, high, comp);
- merge(list,temp, low, mid + 1, high, comp);
- }
- }
-
- private <T> void merge(T[] list, T[]temp, int left, int right, int last, Comparator<T> comp) {
- int j = 0;
- int lowIndex = left;
- int mid = right - 1;
- int n = last - lowIndex + 1;
- while (left <= mid && right <= last){
- if (comp.compare(list[left], list[right]) <0) {
- temp[j++] = list[left++];
- } else {
- temp[j++] = list[right++];
- }
- }
- while (left <= mid) {
- temp[j++] = list[left++];
- }
- while (right <= last) {
- temp[j++] = list[right++];
- }
- for (j = 0; j < n; j++) {
- list[lowIndex + j] = temp[j];
- }
- }
-
- }
QuickSorter.java
- package com.jackfrued.util;
-
- import java.util.Comparator;
-
-
-
-
-
-
-
-
-
-
- public class QuickSorter implements Sorter {
-
- @Override
- public <T extends Comparable<T>> void sort(T[] list) {
- quickSort(list, 0, list.length- 1);
- }
-
- @Override
- public <T> void sort(T[] list, Comparator<T> comp) {
- quickSort(list, 0, list.length- 1, comp);
- }
-
- private <T extends Comparable<T>> void quickSort(T[] list, int first, int last) {
- if (last > first) {
- int pivotIndex = partition(list, first, last);
- quickSort(list, first, pivotIndex - 1);
- quickSort(list, pivotIndex, last);
- }
- }
-
- private <T> void quickSort(T[] list, int first, int last,Comparator<T> comp) {
- if (last > first) {
- int pivotIndex = partition(list, first, last, comp);
- quickSort(list, first, pivotIndex - 1, comp);
- quickSort(list, pivotIndex, last, comp);
- }
- }
-
- private <T extends Comparable<T>> int partition(T[] list, int first, int last) {
- T pivot = list[first];
- int low = first + 1;
- int high = last;
-
- while (high > low) {
- while (low <= high && list[low].compareTo(pivot) <= 0) {
- low++;
- }
- while (low <= high && list[high].compareTo(pivot) >= 0) {
- high--;
- }
- if (high > low) {
- T temp = list[high];
- list[high]= list[low];
- list[low]= temp;
- }
- }
-
- while (high > first&& list[high].compareTo(pivot) >= 0) {
- high--;
- }
- if (pivot.compareTo(list[high])> 0) {
- list[first]= list[high];
- list[high]= pivot;
- return high;
- }
- else {
- return low;
- }
- }
-
- private <T> int partition(T[] list, int first, int last, Comparator<T> comp) {
- T pivot = list[first];
- int low = first + 1;
- int high = last;
-
- while (high > low) {
- while (low <= high&& comp.compare(list[low], pivot) <= 0) {
- low++;
- }
- while (low <= high&& comp.compare(list[high], pivot) >= 0) {
- high--;
- }
- if (high > low) {
- T temp = list[high];
- list[high] = list[low];
- list[low]= temp;
- }
- }
-
- while (high > first&& comp.compare(list[high], pivot) >= 0) {
- high--;
- }
- if (comp.compare(pivot,list[high]) > 0) {
- list[first]= list[high];
- list[high]= pivot;
- return high;
- }
- else {
- return low;
- }
- }
-
- }
138、写一个二分查找(折半搜索)的算法。
答:折半搜索,也称二分查找算法、二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
- package com.jackfrued.util;
-
- import java.util.Comparator;
-
- public class MyUtil {
-
- public static <T extends Comparable<T>> int binarySearch(T[] x, T key) {
- return binarySearch(x, 0, x.length- 1, key);
- }
-
- public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) {
- int low = 0;
- int high = x.length - 1;
- while (low <= high) {
- int mid = (low + high) >>> 1;
- int cmp = comp.compare(x[mid], key);
- if (cmp < 0) {
- low = mid + 1;
- }
- else if (cmp > 0) {
- high = mid - 1;
- }
- else {
- return mid;
- }
- }
- return -1;
- }
-
- private static <T extends Comparable<T>> int binarySearch(T[] x, int low, int high, T key) {
- if(low <= high) {
- int mid = low + ((high -low) >> 1);
- if(key.compareTo(x[mid]) == 0) {
- return mid;
- }
- else if(key.compareTo(x[mid])< 0) {
- return binarySearch(x,l ow, mid - 1, key);
- }
- else {
- return binarySearch(x, mid + 1, high, key);
- }
- }
- return -1;
- }
- }
说明:两个版本一个用递归实现,一个用循环实现。需要注意的是计算中间位置时不应该使用(high+ low) / 2的方式,因为加法运算可能导致整数越界,这里应该使用一下三种方式之一:low+ (high – low) / 2或low + (high – low) >> 1或(low + high) >>> 1(注:>>>是逻辑右移,不带符号位的右移)
139、统计一篇英文文章中单词个数。
答:
- import java.io.FileReader;
-
- public class WordCounting {
-
- public static void main(String[] args) {
- try(FileReader fr = new FileReader("a.txt")) {
- int counter = 0;
- boolean state = false;
- int currentChar;
- while((currentChar= fr.read()) != -1) {
- if(currentChar== ' ' || currentChar == '\n'
- || currentChar == '\t' || currentChar == '\r') {
- state = false;
- }
- else if(!state) {
- state = true;
- counter++;
- }
- }
- System.out.println(counter);
- }
- catch(Exceptione) {
- e.printStackTrace();
- }
- }
- }
补充:这个程序可能有很多种写法,这里选择的是Dennis M. Ritchie和Brian W. Kernighan老师在他们不朽的著作《The C Programming Language》中给出的代码,向两位老师致敬。下面的代码也是如此。
140、输入年月日,计算该日期是这一年的第几天。
答:
- import java.util.Scanner;
-
- public class DayCounting {
-
- public static void main(String[] args) {
- int[][] data = {
- {31,28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
- {31,29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
- };
- Scanner sc = newScanner(System.in);
- System.out.print("请输入年月日(1980 11 28): ");
- int year = sc.nextInt();
- int month = sc.nextInt();
- int date = sc.nextInt();
- int[] daysOfMonth = data[(year % 4 == 0 && year % 100 != 0 || year % 400 == 0)?1 : 0];
- int sum = 0;
- for(int i = 0; i < month -1; i++) {
- sum += daysOfMonth[i];
- }
- sum += date;
- System.out.println(sum);
- sc.close();
- }
- }
141、约瑟夫环:15个基督教徒和15个非教徒在海上遇险,必须将其中一半的人投入海中,其余的人才能幸免于难,于是30个人围成一圈,从某一个人开始从1报数,报到9的人就扔进大海,他后面的人继续从1开始报数,重复上面的规则,直到剩下15个人为止。结果由于上帝的保佑,15个基督教徒最后都幸免于难,问原来这些人是怎么排列的,哪些位置是基督教徒,哪些位置是非教徒。
答:
- public class Josephu {
- private static final int DEAD_NUM = 9;
-
- public static void main(String[] args) {
- boolean[] persons = new boolean[30];
- for(int i = 0; i < persons.length; i++) {
- persons[i] = true;
- }
-
- int counter = 0;
- int claimNumber = 0;
- int index = 0;
- while(counter < 15) {
- if(persons[index]) {
- claimNumber++;
- if(claimNumber == DEAD_NUM) {
- counter++;
- claimNumber= 0;
- persons[index]= false;
- }
- }
- index++;
- if(index >= persons.length) {
- index= 0;
- }
- }
- for(boolean p : persons) {
- if(p) {
- System.out.print("基");
- }
- else {
- System.out.print("非");
- }
- }
- }
- }
142、回文素数:所谓回文数就是顺着读和倒着读一样的数(例如:11,121,1991…),回文素数就是既是回文数又是素数(只能被1和自身整除的数)的数。编程找出11~9999之间的回文素数。
答:
- public class PalindromicPrimeNumber {
-
- public static void main(String[] args) {
- for(int i = 11; i <= 9999; i++) {
- if(isPrime(i) && isPalindromic(i)) {
- System.out.println(i);
- }
- }
- }
-
- public static boolean isPrime(int n) {
- for(int i = 2; i <= Math.sqrt(n); i++) {
- if(n % i == 0) {
- return false;
- }
- }
- return true;
- }
-
- public static boolean isPalindromic(int n) {
- int temp = n;
- int sum = 0;
- while(temp > 0) {
- sum= sum * 10 + temp % 10;
- temp/= 10;
- }
- return sum == n;
- }
- }
143、全排列:给出五个数字12345的所有排列。
答:
- public class FullPermutation {
-
- public static void perm(int[] list) {
- perm(list,0);
- }
-
- private static void perm(int[] list, int k) {
- if (k == list.length) {
- for (int i = 0; i < list.length; i++) {
- System.out.print(list[i]);
- }
- System.out.println();
- }else{
- for (int i = k; i < list.length; i++) {
- swap(list, k, i);
- perm(list, k + 1);
- swap(list, k, i);
- }
- }
- }
-
- private static void swap(int[] list, int pos1, int pos2) {
- int temp = list[pos1];
- list[pos1] = list[pos2];
- list[pos2] = temp;
- }
-
- public static void main(String[] args) {
- int[] x = {1, 2, 3, 4, 5};
- perm(x);
- }
- }
-
144、对于一个有N个整数元素的一维数组,找出它的子数组(数组中下标连续的元素组成的数组)之和的最大值。
答:下面给出几个例子(最大子数组用粗体表示):
1) 数组:{ 1, -2, 3,5, -3, 2 },结果是:8
2) 数组:{ 0, -2, 3, 5, -1, 2 },结果是:9
3) 数组:{ -9, -2,-3, -5, -3 },结果是:-2
可以使用动态规划的思想求解:
- public class MaxSum {
-
- private static int max(int x, int y) {
- return x > y? x: y;
- }
-
- public static int maxSum(int[] array) {
- int n = array.length;
- int[] start = new int[n];
- int[] all = new int[n];
- all[n - 1] = start[n - 1] = array[n - 1];
- for(int i = n - 2; i >= 0;i--) {
- start[i] = max(array[i], array[i] + start[i + 1]);
- all[i] = max(start[i], all[i + 1]);
- }
- return all[0];
- }
-
- public static void main(String[] args) {
- int[] x1 = { 1, -2, 3, 5,-3, 2 };
- int[] x2 = { 0, -2, 3, 5,-1, 2 };
- int[] x3 = { -9, -2, -3,-5, -3 };
- System.out.println(maxSum(x1));
- System.out.println(maxSum(x2));
- System.out.println(maxSum(x3));
- }
- }
145、用递归实现字符串倒转
答:
- public class StringReverse {
-
- public static String reverse(String originStr) {
- if(originStr == null || originStr.length()== 1) {
- return originStr;
- }
- return reverse(originStr.substring(1))+ originStr.charAt(0);
- }
-
- public static void main(String[] args) {
- System.out.println(reverse("hello"));
- }
- }
-
146、输入一个正整数,将其分解为素数的乘积。
答:
- public class DecomposeInteger {
-
- private static List<Integer> list = newArrayList<Integer>();
-
- public static void main(String[] args) {
- System.out.print("请输入一个数: ");
- Scanner sc = newScanner(System.in);
- int n = sc.nextInt();
- decomposeNumber(n);
- System.out.print(n + " = ");
- for(int i = 0; i < list.size() - 1; i++) {
- System.out.print(list.get(i) + " * ");
- }
- System.out.println(list.get(list.size() - 1));
- }
-
- public static void decomposeNumber(int n) {
- if(isPrime(n)) {
- list.add(n);
- list.add(1);
- }
- else {
- doIt(n, (int)Math.sqrt(n));
- }
- }
-
- public static void doIt(int n, int div) {
- if(isPrime(div) && n % div == 0) {
- list.add(div);
- decomposeNumber(n / div);
- }
- else {
- doIt(n, div - 1);
- }
- }
-
- public static boolean isPrime(int n) {
- for(int i = 2; i <= Math.sqrt(n);i++) {
- if(n % i == 0) {
- return false;
- }
- }
- return true;
- }
- }
147、一个有n级的台阶,一次可以走1级、2级或3级,问走完n级台阶有多少种走法。
答:可以通过递归求解。
- public class GoSteps {
-
- public static int countWays(int n) {
- if(n < 0) {
- return 0;
- }
- else if(n == 0) {
- return 1;
- }
- else {
- return countWays(n - 1) + countWays(n - 2) + countWays(n -3);
- }
- }
-
- public static void main(String[] args) {
- System.out.println(countWays(5));
- }
- }
148、写一个算法判断一个英文单词的所有字母是否全都不同(不区分大小写)。
答:
- public class AllNotTheSame {
-
- public static boolean judge(String str) {
- String temp = str.toLowerCase();
- int[] letterCounter = new int[26];
- for(int i = 0; i <temp.length(); i++) {
- int index = temp.charAt(i)- 'a';
- letterCounter[index]++;
- if(letterCounter[index] > 1) {
- return false;
- }
- }
- return true;
- }
-
- public static void main(String[] args) {
- System.out.println(judge("hello"));
- System.out.print(judge("smile"));
- }
- }
149、有一个已经排好序的整数数组,其中存在重复元素,请将重复元素删除掉,例如,A= [1, 1, 2, 2, 3],处理之后的数组应当为A= [1, 2, 3]。
答:
- import java.util.Arrays;
-
- public class RemoveDuplication {
-
- public static int[] removeDuplicates(int a[]) {
- if(a.length <= 1) {
- return a;
- }
- int index = 0;
- for(int i = 1; i < a.length; i++) {
- if(a[index] != a[i]) {
- a[++index] = a[i];
- }
- }
- int[] b = new int[index + 1];
- System.arraycopy(a, 0, b, 0, b.length);
- return b;
- }
-
- public static void main(String[] args) {
- int[] a = {1, 1, 2, 2, 3};
- a = removeDuplicates(a);
- System.out.println(Arrays.toString(a));
- }
- }
150、给一个数组,其中有一个重复元素占半数以上,找出这个元素。
答:
- public class FindMost {
-
- public static <T> T find(T[] x){
- T temp = null;
- for(int i = 0, nTimes = 0; i< x.length;i++) {
- if(nTimes == 0) {
- temp= x[i];
- nTimes= 1;
- }
- else {
- if(x[i].equals(temp)) {
- nTimes++;
- }
- else {
- nTimes--;
- }
- }
- }
- return temp;
- }
-
- public static void main(String[] args) {
- String[]strs = {"hello","kiss","hello","hello","maybe"};
- System.out.println(find(strs));
- }
- }
-
Java面试题集(151-180)
摘要:这部分包含了spring、Spring MVC以及Spring和其他框架整合以及测试相关的内容,除此之外还包含了大型网站技术架构相关面试内容。
151. Spring中的BeanFactory和ApplicationContext有什么联系?
答:Spring通过配置文件描述Bean以及Bean之间的依赖关系,利用Java的反射机制实现Bean的实例化,并建立Bean之间的依赖关系,在此基础上,Spring的IoC容器还提供了Bean实例缓存、生命周期管理、Bean实例代理、事件发布、资源装载等高级服务。BeanFactory是Spring框架最核心的接口,它提供了IoC容器的配置机制。ApplicationContext建立在BeanFactory之上,提供了更多面向应用的功能,包括对国际化和框架事件体系的支持。通常将BeanFactory称为IoC容器,而ApplicationContext称为应用上下文,前者更倾向于Spring本身,后者更倾向于开发者,因此被使用得更多。
【补充】反射(reflection)又叫自省(introspection),是获得对象或类型元数据的方法,Java反射机制可以在运行时判断对象所属的类,在运行时构造任意一个类的对象,在运行时获得一个类的属性和方法,在运行时调用对象的方法,或者生成动态代理。在Java中,可以通过类的Class对象获得类的构造器、属性、方法等类的元数据,还可以访问这些属性或调用这些方法,和反射相关的类还包括:
- Constructor:代表类的构造器的类。通过Class对象的getConstructors方法可以获得类的所有构造器的数组,Java 5以后的版本还可以通过getConstrcutor(Class... parameterTypes)获得拥有特定参数的构造器对象。Constructor对象的一个主要方法是newInstance,通过该方法可以创建一个类的实例。
- Method:代表方法的类。通过Class对象的getDeclaredMethods方法可以获得所有方法的数组,Java 5以后的版本还可以通过getDeclaredMethod(String name, Class... parameterTypes)获得特定签名的方法,其中name是方法名,可变参数代表方法的参数列表。Method对象最重要的方法是invoke(Object obj, Object[] args),其中obj是调用该方法的目标对象,args是传给方法的参数,这样就可以调用指定对象的方法。此外,Method对象还包括以下重要方法:
- Class getReturnType():获取方法的返回类型。
- Class[] getParameterTypes():获取方法参数类型的数组。
- Class[] getExceptionTypes():获取方法异常类型数组。
- Annotation[][] getParameterAnnotations():获得方法参数注解信息的数组。
- Field:代表属性的类。通过Class对象的getDeclaredFields()方法可以获取类的属性数组。通过getDeclaredField(String name)则可以获取某个特定名称的属性。Field对象最重要的方法是set(Object obj, Object value),其中obj是目标对象,而value是要赋给属性的值。
除此之外,Java还提供了Package类用于包的反射,Java5以后的版本还提供了AnnotationElement类用于注解的反射。总之,Java的反射机制保证了可以通过编程的方式访问目标类或对象的所有元素,对于被private和protected访问修饰符修饰的成员,只要JVM的安全机制允许,也可以通过反射进行调用。下面是一个反射的例子:
Car.java
- package com.lovo;
-
- public class Car {
- private String brand;
- private int currentSpeed;
- private int maxSpeed;
-
- public Car(String brand, int maxSpeed) {
- this.brand = brand;
- this.maxSpeed = maxSpeed;
- }
-
- public void run() {
- currentSpeed += 10;
- System.out.println(brand + " is running at " + currentSpeed + " km/h");
- if(currentSpeed > maxSpeed) {
- System.out.println("It's dangerous!");
- }
- }
-
- }
CarTest.java
- package com.lovo;
-
- import java.lang.reflect.Constructor;
- import java.lang.reflect.Field;
- import java.lang.reflect.Method;
-
- public class CarTest {
-
- public static void main(String[] args) throws Exception {
- Constructor<Car> con = Car.class.getConstructor(String.class, int.class);
- Car myCar = (Car) con.newInstance("Benz", 280);
- Method m = myCar.getClass().getDeclaredMethod("run");
- for(int i = 0; i < 10; i++) {
- m.invoke(myCar);
- }
- Field f1 = myCar.getClass().getDeclaredField("maxSpeed");
- Field f2 = myCar.getClass().getDeclaredField("brand");
- f1.setAccessible(true);
- f1.set(myCar, 80);
- f2.setAccessible(true);
- f2.set(myCar, "QQ");
- m.invoke(myCar);
- }
- }
运行结果:
152. Spring中Bean的作用域有哪些?
答:在Spring的早期版本中,仅有两个作用域:singleton和prototype,前者表示Bean以单例的方式存在;后者表示每次从容器中调用Bean时,都会返回一个新的实例,prototype通常翻译为原型,而设计模式中的创建型模式中也有一个原型模式,原型模式也是一个常用的模式,例如做一个室内设计软件,所有的素材都在工具箱中,而每次从工具箱中取出的都是素材对象的一个原型,可以通过对象克隆来实现原型模式。Spring 2.x中针对WebApplicationContext新增了3个作用域,分别是:request(每次HTTP请求都会创建一个新的Bean)、session(同一个HttpSession共享同一个Bean,不同的HttpSession使用不同的Bean)和globalSession(同一个全局Session共享一个Bean)。
需要指出的是:单例模式和原型模式都是重要的设计模式。一般情况下,无状态或状态不可变的类适合使用单例模式。在传统开发中,由于DAO持有Connection这个非线程安全对象因而没有使用单例模式;但在Spring环境下,所有DAO类对可以采用单例模式,因为Spring利用AOP和Java API中的ThreadLocal对非线程安全的对象进行了特殊处理。
【补充】ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。ThreadLocal,顾名思义是线程的一个本地化对象,当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。
ThreadLocal类非常简单好用,只有四个方法,能用上的也就是下面三个方法:
- void set(T value):设置当前线程的线程局部变量的值。
- T get():获得当前线程所对应的线程局部变量的值。
- void remove():删除当前线程中线程局部变量的值。
ThreadLocal是如何做到为每一个线程维护一份独立的变量副本的呢?在ThreadLocal类中有一个Map,键为线程对象,值是其线程对应的变量的副本,自己要模拟实现一个ThreadLocal类其实并不困难,代码如下所示:
- package com.lovo;
-
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.Map;
-
- public class MyThreadLocal<T> {
- private Map<Thread, T> map = Collections.synchronizedMap(new HashMap<Thread, T>());
-
- public void set(T newValue) {
- map.put(Thread.currentThread(), newValue);
- }
-
- public T get() {
- return map.get(Thread.currentThread());
- }
-
- public void remove() {
- map.remove(Thread.currentThread());
- }
- }
153. 你如何理解AOP中的连接点(Joinpoint)、切点(Pointcut)、增强(Advice)、引介(Introduction)、织入(Weaving)、切面(Aspect)这些概念?
答:
- 连接点:程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring仅支持方法的连接点。
- 切点:如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。
- 增强:增强是织入到目标类连接点上的一段程序代码。Spring提供的增强接口都是带方位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。很多资料上将增强译为“通知”,这明显是个词不达意的翻译,让很多程序员困惑了许久。
- 引介:引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过引介功能,可以动态的未该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
- 织入:织入是将增强添加到目标类具体连接点上的过程,AOP有三种织入方式:
- 编译期织入:需要特殊的Java编译期(例如AspectJ的ajc)
- 类装载期织入:要求使用特殊的类加载器
- 动态代理织入:在运行时为目标类生成代理实现增强
Spring采用了动态代理织入,而AspectJ采用了编译期织入和类装载期织入的方式。
- 切面:切面是由切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义。
【补充】代理模式是GoF提出的23种设计模式中最为经典的模式之一,代理模式是对象的结构模式,它给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。简单的说,代理对象可以完成比原对象更多的职责,当需要为原对象添加横切关注功能时,就可以使用原对象的代理对象。我们在打开Office系列的Word文档时,如果文档中有插图,当文档刚加载时,文档中的插图都只是一个虚框占位符,等用户真正翻到某页要查看该图片时,才会真正加载这张图,这其实就是对代理模式的使用,代替真正图片的虚框就是一个虚拟代理;hibernate的load方法也是返回一个虚拟代理对象,等用户真正需要访问对象的属性时,才向数据库发出SQL语句获得真实对象。代理模式的类图如下所示:
下面用一个找枪手代考的例子演示代理模式的使用:
- package com.lovo;
-
-
-
-
-
-
- public interface Candidate {
-
-
-
-
- public void answerTheQuestions();
- }
- package com.lovo;
-
-
-
-
-
-
- public class LazyStudent implements Candidate {
- private String name;
-
- public LazyStudent(String name) {
- this.name = name;
- }
-
- @Override
- public void answerTheQuestions() {
-
- System.out.println("姓名: " + name);
- }
-
- }
- package com.lovo;
-
-
-
-
-
-
- public class Gunman implements Candidate {
- private Candidate target;
-
- public Gunman(Candidate target) {
- this.target = target;
- }
-
- @Override
- public void answerTheQuestions() {
-
- target.answerTheQuestions();
-
- System.out.println("奋笔疾书正确答案");
- System.out.println("交卷");
- }
-
- }
- package com.lovo;
-
- public class ProxyTest1 {
-
- public static void main(String[] args) {
- Candidate c = new Gunman(new LazyStudent("王小二"));
- c.answerTheQuestions();
- }
- }
从JDK 1.3开始,Java提供了动态代理技术,允许开发者在运行时创建接口的代理实例,主要包括Proxy类和InvocationHandler接口。下面的例子使用动态代理为ArrayList编写一个代理,在添加和删除元素时,在控制台打印添加或删除的元素以及ArrayList的大小:
- package com.lovo;
-
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- import java.util.List;
-
- public class ListProxy<T> implements InvocationHandler {
- private List<T> target;
-
- public ListProxy(List<T> target) {
- this.target = target;
- }
-
- @Override
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- Object retVal = null;
- System.out.println("[" + method.getName() + ": " + args[0] + "]");
- retVal = method.invoke(target, args);
- System.out.println("[size=" + target.size() + "]");
- return retVal;
- }
-
- }
- package com.lovo;
-
- import java.lang.reflect.Proxy;
- import java.util.ArrayList;
- import java.util.List;
-
- public class ProxyTest2 {
-
- @SuppressWarnings("unchecked")
- public static void main(String[] args) {
- List<String> list = new ArrayList<String>();
- Class<?> clazz = list.getClass();
- ListProxy<String> myProxy = new ListProxy<String>(list);
- List<String> newList = (List<String>)
- Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), myProxy);
- newList.add("apple");
- newList.add("banana");
- newList.add("orange");
- newList.remove("banana");
- }
- }
程序运行结果:
使用Java的动态代理有一个局限性就是代理的类必须要实现接口,虽然面向接口编程是每个优秀的Java程序都知道的规则,但现实往往不尽如人意,对于没有实现接口的类如何为其生成代理呢?继承!继承是最经典的扩展已有代码能力的手段,虽然继承常常被初学者滥用,但继承也常常被进阶的程序员忽视。CGLib采用非常底层的字节码生成技术,通过为一个类创建子类来生成代理,它弥补了Java动态代理的不足,因此Spring中动态代理和CGLib都是创建代理的重要手段,对于实现了接口的类就用动态代理为其生成代理类,而没有实现接口的类就用CGLib通过继承的方式为其创建代理。
154. Spring中自动装配的方式有哪些?
答:
- no:不进行自动装配,手动设置Bean的依赖关系
- byName:根据Bean的名字进行自动装配
- byType:根据Bean的类型进行自动装配
- constructor:类似于byType,不过是应用于构造器的参数,如果正好有一个Bean与构造器的参数类型相同则可以自动装配,否则会导致错误
- autodetect:如果有默认的构造器,则通过constructor的方式进行自动装配,否则使用byType的方式进行自动装配
【注意】自动装配没有自定义装配方式那么精确,而且不能自动装配简单属性(基本类型、字符串等),在使用时应注意。
155. Spring中如何使用注解来配置Bean?有哪些相关的注解?
答:首先需要在Spring配置文件中增加如下配置:
- <context:component-scan base-package="org.example"/>
然后可以用@Component、@Controller、@Service、@Repository注解来标注需要由Spring IoC容器进行对象托管的类。
156. Spring支持的事务管理类型有哪些?你在项目中使用哪种方式?
答:Spring支持编程式事务管理和声明式事务管理。许多Spring框架的用户选择声明式事务管理,因为这种方式和应用程序的关联较少,因此更加符合轻量级容器的概念。声明式事务管理要优于编程式事务管理,尽管在灵活性方面它弱于编程式事务管理(编程式事务允许你通过代码控制业务)。
157. 如何在Web项目中配置Spring的IoC容器?
答:如果需要在Web项目中使用Spring的IoC容器,可以在Web项目配置文件web.xml中做出如下配置:
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
- </context-param>
-
- <listener>
- <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
- </listener>
158. 如何在Web项目中配置Spring MVC?
答:要使用Spring MVC需要在Web项目配置文件中配置其前端控制器DispatcherServlet,如下所示:
- <web-app>
-
- <servlet>
- <servlet-name>example</servlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- <load-on-startup>1</load-on-startup>
- </servlet>
-
- <servlet-mapping>
- <servlet-name>example</servlet-name>
- <url-pattern>*.html</url-pattern>
- </servlet-mapping>
-
- </web-app>
【注意】上面的配置中使用了*.html的后缀映射,这样做一方面不能够通过URL推断采用了何种服务器端的技术,另一方面可以欺骗搜索引擎,因为搜索引擎不会搜索动态页面,这种做法可以称为伪静态化。
159. Spring MVC的工作原理是怎样的?
答:Spring MVC的工作原理如下图所示:
- 客户端的所有请求都交给前端控制器DispatcherServlet来处理,它会负责调用系统的其他模块来真正处理用户的请求。
- DispatcherServlet收到请求后,将根据请求的信息(包括URL、HTTP协议方法、请求头、请求参数、Cookie等)以及HandlerMapping的配置找到处理该请求的Handler(任何一个对象都可以作为请求的Handler)。当然,在这个地方Spring会通过HandlerAdapter对该处理器进行封装,HandlerAdapter是一个适配器,它用统一的接口对各种Handler中的方法进行调用。
- Handler完成对用户请求的处理后,会返回一个ModelAndView对象给DispatcherServlet,ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。当然,这里的视图是逻辑视图,DispatcherServlet还要借助ViewResolver完成从逻辑视图到真实视图对象的解析工作。
- 当得到真正的视图对象后,Dispatcher会利用视图对象对模型数据进行渲染。
- 客户端得到响应,可能是一个普通的HTML页面,也可以是XML或JSON字符串,还可以是一张图片或者一个PDF文件。
160. 如何在Spring IoC容器中配置数据源?
答:
- <bean id="dataSource"
- class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="driverClassName" value="${jdbc.driverClassName}"/>
- <property name="url" value="${jdbc.url}"/>
- <property name="username" value="${jdbc.username}"/>
- <property name="password" value="${jdbc.password}"/>
- </bean>
-
- <context:property-placeholder location="jdbc.properties"/>
- <bean id="dataSource"
- class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
- <property name="driverClass" value="${jdbc.driverClassName}"/>
- <property name="jdbcUrl" value="${jdbc.url}"/>
- <property name="user" value="${jdbc.username}"/>
- <property name="password" value="${jdbc.password}"/>
- </bean>
-
- <context:property-placeholder location="jdbc.properties"/>
161. 如何配置配置事务增强?
答:
- <?xml version="1.0" encoding="UTF-8"?>
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:aop="http://www.springframework.org/schema/aop"
- xmlns:tx="http://www.springframework.org/schema/tx"
- xsi:schemaLocation="
- http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans.xsd
- http://www.springframework.org/schema/tx
- http://www.springframework.org/schema/tx/spring-tx.xsd
- http://www.springframework.org/schema/aop
- http://www.springframework.org/schema/aop/spring-aop.xsd">
-
-
- <bean id="fooService" class="x.y.service.DefaultFooService"/>
-
-
- <tx:advice id="txAdvice" transaction-manager="txManager">
-
- <tx:attributes>
-
- <tx:method name="get*" read-only="true"/>
-
- <tx:method name="*"/>
- </tx:attributes>
- </tx:advice>
-
- <!-- ensure that the above transactional advice runs for any execution
- of an operation defined by the FooService interface -->
- <aop:config>
- <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
- <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
- </aop:config>
-
-
- <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
- <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
- <property name="username" value="scott"/>
- <property name="password" value="tiger"/>
- </bean>
-
-
- <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
- <property name="dataSource" ref="dataSource"/>
- </bean>
-
-
-
- </beans>
162. 选择使用Spring框架的原因(Spring框架为企业级开发带来的好处)?
答:可以从以下几个方面作答:
- 非侵入式:支持基于POJO的编程模式,不强制性的要求实现Spring框架中的接口或继承Spring框架中的类。
- IoC容器:IoC容器帮助应用程序管理对象以及对象之间的依赖关系,对象之间的依赖关系如果发生了改变只需要修改配置文件而不是修改代码,因为代码的修改可能意味着项目的重新构建和完整的回归测试。有了IoC容器,程序员再也不需要自己编写工厂、单例,这一点特别符合Spring的精神“不要重复的发明轮子”。
- AOP:面向切面编程,将所有的横切关注功能封装到切面(aspect)中,通过配置的方式将横切关注功能动态添加到目标代码上,进一步实现了业务逻辑和系统服务之间的分离。另一方面,有了AOP程序员可以省去很多自己写代理类的工作。
- MVC:Spring的MVC框架是非常优秀的,从各个方面都可以甩Struts 2几条街,为Web表示层提供了更好的解决方案。
- 事务管理:Spring以宽广的胸怀接纳多种持久层技术(我想这一点与Rod Johnson音乐学博士的身份不无关系,这一点特别值得Gavin King学习,上天不可能赋予一个人太多的优点以至于他没有表达谦虚的余地。关于这二位的轶事可以自己百度一下),并且为其提供了声明式的事务管理,在不需要任何一行代码的情况下就能够完成事务管理。
- 其他:选择Spring框架的原因还远不止于此,Spring为Java企业级开发提供了一站式选择,你可以在需要的时候使用它的部分和全部,更重要的是,你甚至可以在感觉不到Spring存在的情况下,在你的项目中使用Spring提供的各种优秀的功能。
163. 依赖注入的方式以及你在项目中的选择?
答:依赖注入可以通过setter方法注入(设值注入)、构造器注入和接口注入三种方式来实现,Spring支持setter注入和构造器注入,通常使用构造器注入来注入必须的依赖关系,对于可选的依赖关系,则setter注入是更好的选择,setter注入需要类提供无参构造器或者无参的静态工厂方法来创建对象。
164. 提供Spring IoC容器配置元数据的方式?
答:
- 基于XML文件进行配置。
- 基于注解进行配置。
- 基于Java程序进行配置(Spring 3+)
- package com.lovo.bean;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
-
- @Component
- public class Person {
- private String name;
- private int age;
- @Autowired
- private Car car;
-
- public Person(String name, int age) {
- this.name = name;
- this.age = age;
- }
-
- public void setCar(Car car) {
- this.car = car;
- }
-
- @Override
- public String toString() {
- return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
- }
-
- }
- package com.lovo.bean;
-
- import org.springframework.stereotype.Component;
-
- @Component
- public class Car {
- private String brand;
- private int maxSpeed;
-
- public Car(String brand, int maxSpeed) {
- this.brand = brand;
- this.maxSpeed = maxSpeed;
- }
-
- @Override
- public String toString() {
- return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
- }
-
- }
- package com.lovo.config;
-
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
-
- import com.lovo.bean.Car;
- import com.lovo.bean.Person;
-
- @Configuration
- public class AppConfig {
-
- @Bean
- public Car car() {
- return new Car("Benz", 320);
- }
-
- @Bean
- public Person person() {
- return new Person("骆昊", 34);
- }
- }
- package com.lovo.test;
-
- import org.springframework.context.ConfigurableApplicationContext;
- import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-
- import com.lovo.bean.Person;
- import com.lovo.config.AppConfig;
-
- class Test {
-
- public static void main(String[] args) {
-
- try(ConfigurableApplicationContext factory = new AnnotationConfigApplicationContext(AppConfig.class)) {
- Person person = factory.getBean(Person.class);
- System.out.println(person);
- }
- }
- }
165. Spring提供了哪些Bean的作用域?
答:
- singleton:Bean以单例的方式存在(IoC容器中仅存在该Bean的唯一实例)。
- prototype:每次从容器中获取Bean的实例时,都会返回一个新的实例(原型模式)。
- request:每次HTTP请求都会创建新的实例,该作用域仅适用于WebApplicationContext环境。
- session:同一个HTTP会话共享一个Bean的实例,不同的HTTP会话使用不同的Bean实例,该作用域仅适用于WebApplicationContext环境。
- globalSession:同一个全局会话共享一个Bean的实例,一般应用于Portlet应用中。
166. 阐述Spring框架中Bean的生命周期?
答:
- Spring IoC容器找到关于Bean的定义并实例化该Bean。
- Spring IoC容器对Bean进行依赖注入。
- 如果Bean实现了BeanNameAware接口,则将该Bean的id传给setBeanName方法。
- 如果Bean实现了BeanFactoryAware接口,则将BeanFactory对象传给setBeanFactory方法。
- 如果Bean实现了BeanPostProcessor接口,则调用其postProcessBeforeInitialization方法。
- 如果Bean实现了InitializingBean接口,则调用其afterPropertySet方法。
- 如果有和Bean关联的BeanPostProcessors对象,则这些对象的postProcessAfterInitialization方法被调用。
- 当销毁Bean实例时,如果Bean实现了DisposableBean接口,则调用其destroy方法。
167. 依赖注入时如何注入集合属性?
答:可以在定义Bean属性时,通过<list>/<set>/<map>/<props>分别为其注入列表、集合、映射和键值都是字符串的映射属性。
168. Spring中的自动装配有哪些限制?
答:
- 如果使用了构造器注入或者setter注入,那么将覆盖自动装配的依赖关系。
- 基本数据类型的值、字符串字面量、类字面量无法使用自动装配来注入。
- 有先考虑使用显式的装配来进行更精确的依赖注入而不是使用自动装配。
169. 和自动装配相关的注解有哪些?
答:
- @Required:该依赖关系必须装配(手动或自动装配),否则将抛出BeanInitializationException异常。
- @Autowired:自动装配,默认按类型进行自动装配。
- @Qualifier:如果按类型自动装配时有不止一个匹配的类型,那么可以使用该注解指定名字来消除歧义。
170. 如何使用HibernateDaoSupport整合Spring和Hibernate?
答:
- 在Spring中配置Hibernate的会话工厂(LocalSessionFactoryBean或AnnotationSessionFactoryBean)。
- 让DAO的实现类继承HibernateDaoSupport(继承getHibernateTemplate方法来调用模板方法)。
- 让Spring来管理Hibernate的事务(推荐使用声明式事务)。
171. 你是如何理解“横切关注”这个概念的?
答:“横切关注”是会影响到整个应用程序的关注功能,它跟正常的业务逻辑是正交的,没有必然的联系,但是几乎所有的业务逻辑都会涉及到这些关注功能。通常,事务、日志、安全性等关注就是应用中的横切关注功能。
172. 如何理解Spring AOP中Advice这个概念?
答:Advice在国内的很多书面资料中都被翻译成“通知”,但是很显然这个翻译无法表达其本质,有少量的读物上将这个词翻译为“增强”,这个翻译是对Advice较为准确的诠释,我们通过AOP将横切关注功能加到原有的业务逻辑上,这就是对原有业务逻辑的一种增强,这种增强可以是前置增强、后置增强、返回后增强、抛异常时增强和包围型增强。
173. 在Web项目中如何获得Spring的IoC容器?
答:
- WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
174. Spring MVC如何对RESTful风格提供支持?
答:如果不了解RESTful可以看看百度百科的讲解,关于这个问题,可以看看blogjava上的另一个帖子。
175. 大型网站在架构上应当考虑哪些问题?
答:
- 分层:分层是处理任何复杂系统最常见的手段之一,将系统横向切分成若干个层面,每个层面只承担单一的职责,然后通过下层为上层提供的基础设施和服务以及上层对下层的调用来形成一个完整的复杂的系统。计算机网络的开放系统互联参考模型(OSI/RM)和Internet的TCP/IP模型都是分层结构,大型网站的软件系统也可以使用分层的理念将其分为持久层(提供数据存储和访问服务)、业务层(处理业务逻辑,系统中最核心的部分)和表示层(系统交互、视图展示)。需要指出的是:(1)分层是逻辑上的划分,在物理上可以位于同一设备上也可以在不同的设备上部署不同的功能模块,这样可以使用更多的计算资源来应对用户的并发访问;(2)层与层之间应当有清晰的边界,这样分层才有意义,才更利于软件的开发和维护。
- 分割:分割是对软件的纵向切分。我们可以将大型网站的不同功能和服务分割开,形成高内聚低耦合的功能模块(单元)。在设计初期可以做一个粗粒度的分割,将网站分割为若干个功能模块,后期还可以进一步对每个模块进行细粒度的分割,这样一方面有助于软件的开发和维护,另一方面有助于分布式的部署,提供网站的并发处理能力和功能的扩展。
- 分布式:除了上面提到的内容,网站的静态资源(JavaScript、CSS、图片等)也可以采用独立分布式部署并采用独立的域名,这样可以减轻应用服务器的负载压力,也使得浏览器对资源的加载更快。数据的存取也应该是分布式的,传统的商业级关系型数据库产品基本上都支持分布式部署,而新生的NoSQL产品几乎都是分布式的。当然,网站后台的业务处理也要使用分布式技术,例如查询索引的构建、数据分析等,这些业务计算规模庞大,可以使用Hadoop以及MapReduce分布式计算框架来处理。
- 集群:集群使得有更多的服务器提供相同的服务,可以更好的提供对并发的支持。
- 缓存:所谓缓存就是用空间换取时间的技术,将数据尽可能放在距离计算最近的位置。使用缓存是网站优化的第一定律。我们通常说的CDN、反向代理、热点数据都是对缓存技术的使用。
- 异步:异步是实现软件实体之间解耦合的又一重要手段。异步架构是典型的生产者消费者模式,二者之间没有直接的调用关系,只要保持数据结构不变,彼此功能实现可以随意变化而不互相影响,这对网站的扩展非常有利。使用异步处理还可以提高系统可用性,加快网站的响应速度(用Ajax加载数据就是一种异步技术),同时还可以起到削峰作用(应对瞬时高并发)。“能推迟处理的都要推迟处理”是网站优化的第二定律,而异步是践行网站优化第二定律的重要手段。
- 冗余:各种服务器都要提供相应的冗余服务器以便在某台或某些服务器宕机时还能保证网站可以正常工作,同时也提供了灾难恢复的可能性。冗余是网站高可用性的重要保证。
176. 你用过的网站前端优化的技术有哪些?
答:
- 浏览器访问优化
- 减少HTTP请求数量:合并CSS、合并JavaScript、合并图片(CSS Sprite)
- 使用浏览器缓存:通过设置HTTP响应头中的Cache-Control和Expires属性,将CSS、JavaScript、图片等在浏览器中缓存,当这些静态资源需要更新时,可以更新HTML文件中的引用来让浏览器重新请求新的资源
- 启用压缩
- CSS前置,JavaScript后置
- 减少Cookie传输
- CDN加速:CDN(Content Distribute Network)的本质仍然是缓存,将数据缓存在离用户最近的地方,CDN通常部署在网络运营商的机房,不仅可以提升响应速度,还可以减少应用服务器的压力。当然,CDN缓存的通常都是静态资源。
- 反向代理:反向代理相当于应用服务器的一个门面,可以保护网站的安全性,也可以实现负载均衡的功能,当然最重要的是它缓存了用户访问的热点资源,可以直接从反向代理将某些内容返回给用户浏览器。
177. 你使用过的应用服务器优化技术有哪些?
答:
- 分布式缓存:缓存的本质就是内存中的哈希表,如果设计一个优质的哈希函数,那么理论上哈希表读写的渐近时间复杂度为O(1)。缓存主要用来存放那些读写比很高、变化很少的数据,这样应用程序读取数据时先到缓存中读取,如果没有或者数据已经失效再去访问数据库或文件系统,并根据拟定的规则将数据写入缓存。对网站数据的访问也符合二八定律(Pareto分布,幂律分布),即80%的访问都集中在20%的数据上,如果能够将这20%的数据缓存起来,那么系统的性能将得到显著的改善。当然,使用缓存需要解决以下几个问题:(1)频繁修改的数据;(2)数据不一致与脏读;(3)缓存雪崩(可以采用分布式缓存服务器集群加以解决,Memcached是广泛采用的解决方案,它是一种互不通信的集中式管理的分布式缓存方案,可以从http://memcached.org/了解到关于Memcached的相关信息);(4)缓存预热;(5)缓存穿透(恶意持续请求不存在的数据)。
- 异步操作:可以使用消息队列将调用异步化,通过异步处理将短时间高并发产生的事件消息存储在消息队列中,从而起到削峰作用。电商网站在进行促销活动时,可以将用户的订单请求存入消息队列,这样可以抵御大量的并发订单请求对系统和数据库的冲击。目前,绝大多数的电商网站即便不进行促销活动,订单系统都采用了消息队列来处理。
- 使用集群
- 代码优化
- 多线程:基于Java的Web开发基本上都通过多线程的方式响应用户的并发请求,使用多线程技术在编程上要解决线程安全问题,主要可以考虑以下几个方面:
- 将对象设计为无状态对象(这和面向对象的编程观点是矛盾的,在面向对象的世界中被视为不良设计),这样就不会存在并发访问时对象状态不一致的问题。
- 在方法内部创建对象,这样对象由进入方法的线程创建,不会出现多个线程访问同一对象的问题。使用ThreadLocal将对象与线程绑定也是很好的做法,这一点在前面已经探讨过了。
- 对资源进行并发访问时应当使用合理的锁机制。
- 非阻塞I/O: 使用单线程和非阻塞I/O是目前公认的比多线程的方式更能充分发挥服务器性能的应用模式,基于Node.js构建的服务器就采用了这样的方式。Java在JDK 1.4中就引入了NIO(Non-blocking I/O),在Servlet 3规范中又引入了异步Servlet的概念,这些都为在服务器端采用非阻塞I/O提供了必要的基础。
- 资源复用:资源复用主要有两种方式,一是单例,而是对象池,我们使用的数据库连接池、线程池都是对象池化技术,这是典型的用空间换取时间的策略,另一方面也实现对资源的复用,从而避免了不必要的创建和释放资源所带来的开销。
178. 什么是XSS攻击?什么是SQL注入攻击?什么是CSRF攻击?
答:
- XSS(Cross Site Script,跨站脚本攻击)是向网页中注入恶意脚本在用户浏览网页时在用户浏览器中执行恶意脚本的攻击方式。跨站脚本攻击分有两种形式:反射型攻击(诱使用户点击一个嵌入恶意脚本的链接以达到攻击的目标,目前有很多攻击者利用论坛、微博发布含有恶意脚本的URL就属于这种方式)和持久型攻击(将恶意脚本提交到被攻击网站的数据库中,用户浏览网页时,恶意脚本从数据库中被加载到页面执行,QQ邮箱的早期版本就曾经被利用作为持久型跨站脚本攻击的平台)。XSS虽然不是什么新鲜玩意,但是攻击的手法却不断翻新,防范XSS主要有两方面:消毒(对危险字符进行转义)和HttpOnly(防范XSS攻击者窃取Cookie数据)。
- SQL注入攻击是注入攻击最常见的形式(此外还有OS注入攻击(Struts 2的高危漏洞就是通过OGNL实施OS注入攻击导致的)),当服务器使用请求参数构造SQL语句时,恶意的SQL被嵌入到SQL中交给数据库执行。SQL注入攻击需要攻击者对数据库结构有所了解才能进行,攻击者想要获得表结构有多种方式:(1)如果使用开源系统搭建网站,数据库结构也是公开的(目前有很多现成的系统可以直接搭建论坛,电商网站,虽然方便快捷但是风险是必须要认真评估的);(2)错误回显(如果将服务器的错误信息直接显示在页面上,攻击者可以通过非法参数引发页面错误从而通过错误信息了解数据库结构,Web应用应当设置友好的错误页,一方面符合最小惊讶原则,一方面屏蔽掉可能给系统带来危险的错误回显信息);(3)盲注。防范SQL注入攻击也可以采用消毒的方式,通过正则表达式对请求参数进行验证,此外,参数绑定也是很好的手段,这样恶意的SQL会被当做SQL的参数而不是命令被执行,JDBC中的PreparedStatement就是支持参数绑定的语句对象,从性能和安全性上都明显优于Statement。
- CSRF攻击(Cross Site Request Forgery,跨站请求伪造)是攻击者通过跨站请求,以合法的用户身份进行非法操作(如转账或发帖等)。CSRF的原理是利用浏览器的Cookie或服务器的Session,盗取用户身份,其原理如下图所示。防范CSRF的主要手段是识别请求者的身份,主要有以下几种方式:(1)在表单中添加令牌(token);(2)验证码;(3)检查请求头中的Referer(前面提到防图片盗链接也是用的这种方式)。令牌和验证都具有一次消费性的特征,因此在原理上一致的,但是验证码是一种糟糕的用户体验,不是必要的情况下不要轻易使用验证码,目前很多网站的做法是如果在短时间内多次提交一个表单未获得成功后才要求提供验证码,这样会获得较好的用户体验。
【补充】防火墙的架设是Web安全的重要保障,ModSecurity是开源的Web防火墙中的佼佼者,有兴趣的可以在http://www.modsecurity.org/网站获得相关信息。企业级防火墙的架设应当有两级防火墙,Web服务器和部分应用服务器可以架设在两级防火墙之间的DMZ,而数据和资源服务器应当架设在第二级防火墙之后,如下图所示。
179. 什么是领域模型(domain model)?贫血模型(anaemic domain model)
和充血模型(rich domain model)有什么区别?
答:领域模型是领域内的概念类或现实世界中对象的可视化表示,又称为概念模型或分析对象模型,它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。贫血模型是指使用的领域对象中只有setter和getter方法(POJO),所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。有人将我们这里说的贫血模型进一步划分成失血模型(领域对象完全没有业务逻辑)和贫血模型(领域对象有少量的业务逻辑),我们这里就不对此加以区分了。充血模型将大多数业务逻辑和持久化放在领域对象中,业务逻辑(业务门面)只是完成对业务逻辑的封装、事务和权限等的处理。下面两张图分别展示了贫血模型和充血模型的分层架构。
贫血模型
充血模型
贫血模型下组织领域逻辑通常使用事务脚本模式,让每个过程对应用户可能要做的一个动作,每个动作由一个过程来驱动。也就是说在设计业务逻辑接口的时候,每个方法对应着用户的一个操作,这种模式有以下几个有点:
- 它是一个大多数开发者都能够理解的简单过程模型(适合国内的绝大多数开发者)。
- 它能够与一个使用行数据入口或表数据入口的简单数据访问层很好的协作。
- 事务边界的显而易见,一个事务开始于脚本的开始,终止于脚本的结束,很容易通过代理(或切面)实现声明式事务。
然后,事务脚本模式的缺点也是很多的,随着领域逻辑复杂性的增加,系统的复杂性将迅速增加,程序结构将变得极度混乱。开源中国社区上有一篇很好的译文《贫血领域模型是如何导致糟糕的软件产生》对这个问题做了比较细致的阐述。
180. 谈一谈测试驱动开发(TDD)的好处以及你的理解。
答:TDD是指在编写真正的功能实现代码之前先写测试代码,然后根据需要重构实现代码。在JUnit的作者Kent Beck的大作《测试驱动开发:实战与模式解析》(Test-Driven Development: by Example)一书中有这么一段内容:“消除恐惧和不确定性是编写测试驱动代码的重要原因”。因为编写代码时的恐惧会让你小心试探,让你回避沟通,让你羞于得到反馈,让你变得焦躁不安,而TDD是消除恐惧、让Java开发者更加自信更加乐于沟通的重要手段。TDD会带来的好处可能不会马上呈现,但是你在某个时候一定会发现,这些好处包括:
- 更清晰的代码 --- 只写需要的代码
- 更好的设计
- 更出色的灵活性 --- 鼓励程序员面向接口编程
- 更快速的反馈 --- 不会到系统上线时才知道bug的存在
【补充:】敏捷软件开发的概念已经有很多年了,而且也部分的改变了软件开发这个行业,TDD也是敏捷开发所倡导的。
TDD可以在多个层级上应用,包括单元测试(测试一个类中的代码)、集成测试(测试类之间的交互)、系统测试(测试运行的系统)和系统集成测试(测试运行的系统包括使用的第三方组件)。TDD的实施步骤是:红(失败测试) --- 绿(通过测试) --- 重构。关于实施TDD的详细步骤请参考另一篇文章《测试驱动开发之初窥门径》。
在使用TDD开发时,经常会遇到需要被测对象需要依赖其他子系统的情况,但是你希望将测试代码跟依赖项隔离,以保证测试代码仅仅针对当前被测对象或方法展开,这时候你需要的是测试替身。测试替身可以分为四类:
- 虚设替身:只传递但是不会使用到的对象,一般用于填充方法的参数列表
- 存根替身:总是返回相同的预设响应,其中可能包括一些虚设状态
- 伪装替身:可以取代真实版本的可用版本(比真实版本还是会差很多)
- 模拟替身:可以表示一系列期望值的对象,并且可以提供预设响应
Java世界中实现模拟替身的第三方工具非常多,包括EasyMock、Mockito、jMock等。