JAVA面试题
摘自:http://www.importnew.com/22083.html
- float f=3.4;是否正确?
答:不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。 - 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);其中有隐含的强制类型转换。 - int和Integer有什么区别?
答:Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
- 原始类型: boolean,char,byte,short,int,long,float,double
- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double12345678910class
AutoUnboxingTest {
public
static
void
main(String[] args) {
Integer a =
new
Integer(
3
);
Integer b =
3
;
// 将3自动装箱成Integer类型
int
c =
3
;
System.out.println(a == b);
// false 两个引用没有引用同一对象
System.out.println(a == c);
// true a自动拆箱成int类型再和c比较
}
}
最近还遇到一个面试题,也是和自动装箱和拆箱有点关系的,代码如下所示:
123456789public
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的源代码就知道发生了什么。
12345public
static
Integer valueOf(
int
i) {
if
(i >= IntegerCache.low && i <= IntegerCache.high)
return
IntegerCache.cache[i + (-IntegerCache.low)];
return
new
Integer(i);
}
IntegerCache是Integer的内部类,其代码如下所示:
1234567891011121314151617181920212223242526272829303132333435363738394041424344/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private
static
class
IntegerCache {
static
final
int
low = -
128
;
static
final
int
high;
static
final
Integer cache[];
static
{
// high value may be configured by property
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
);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -
1
);
}
catch
( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache =
new
Integer[(high - low) +
1
];
int
j = low;
for
(
int
k =
0
; k < cache.length; k++)
cache[k] =
new
Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert
IntegerCache.high >=
127
;
}
private
IntegerCache() {}
}
简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2的结果是true,而f3==f4的结果是false。
- 解释内存中的栈(stack)、堆(heap)和静态区(static area)的用法。
答:通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;而通过new关键字和构造器创建的对象放在堆空间;程序中的字面量(literal)如直接书写的100、”hello”和常量都是放在静态区中。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,理论上整个内存没有被其他进程使用的空间甚至硬盘上的虚拟内存都可以被当成堆空间来使用。1String str =
new
String(
"hello"
);
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而”hello”这个字面量放在静态区。
补充:较新版本的Java(从Java 6的某个更新开始)中使用了一项叫”逃逸分析”的技术,可以将一些局部对象放在栈上以提升对象的操作性能。
- Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。 - switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
答:在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。 - 在Java中,如何跳出当前的多重嵌套循环?
答:在最外层循环前加一个标记如A,然后用break A;可以跳出多重循环。(Java中支持带标签的break和continue语句,作用有点类似于C和C++中的goto语句,但是就像要避免使用goto一样,应该避免使用带标签的break和continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好) - 两个对象值相同(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注解。
- 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
答:是值传递。Java语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和C#中可以通过传引用或传输出参数来改变传入的参数的值。在C#中可以编写如下所示的代码,但是在Java中却做不到。12345678910111213141516171819using System;
namespace CS01 {
class
Program {
public
static
void
swap(ref
int
x, ref
int
y) {
int
temp = x;
x = y;
y = temp;
}
public
static
void
Main (string[] args) {
int
a =
5
, b =
10
;
swap (ref a, ref b);
// a = 10, b = 5;
Console.WriteLine (
"a = {0}, b = {1}"
, a, b);
}
}
}
说明:Java中没有传引用实在是非常的不方便,这一点在Java 8中仍然没有得到改进,正是如此在Java编写的代码中才会出现大量的Wrapper类(将需要通过方法调用修改的引用置于一个Wrapper类中,再将Wrapper对象传入方法),这样的做法只会让代码变得臃肿,尤其是让从C和C++转型为Java程序员的开发者无法容忍。
- String和StringBuilder、StringBuffer的区别?
答:Java平台提供了两种类型的字符串:String和StringBuffer/StringBuilder,它们可以储存和操作字符串。其中String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。而StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder是Java 5中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer要高。面试题1 - 什么情况下用+运算符进行字符串连接比调用StringBuffer/StringBuilder对象的append方法连接字符串性能更好?
面试题2 - 请说出下面程序的输出。
1234567891011class
StringEqualTest {
public
static
void
main(String[] args) {
String s1 =
"Programming"
;
String s2 =
new
String(
"Programming"
);
String s3 =
"Program"
+
"ming"
;
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1 == s1.intern());
}
}
补充:String对象的intern方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String对象的equals结果是true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用。
- 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。面试题:华为的面试题中曾经问过这样一个问题 – “为什么不能根据返回类型来区分重载”,快说出你的答案吧!
- 描述一下JVM加载class文件的原理机制?
答:JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:- Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
- Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
- System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
- 抽象类(abstract class)和接口(interface)有什么异同?
答:抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。 - 静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?
答:Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化,其语法看起来挺诡异的,如下所示。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475/**
* 扑克类(一副扑克)
* @author 骆昊
*
*/
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;
}
}
/**
* 发牌
* @param index 发牌的位置
*
*/
public
Card deal(
int
index) {
return
cards[index];
}
/**
* 卡片类(一张扑克)
* [内部类]
* @author 骆昊
*
*/
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;
}
}
}
测试代码:
1234567891011121314class
PokerTest {
public
static
void
main(String[] args) {
Poker poker =
new
Poker();
poker.shuffle();
// 洗牌
Poker.Card c1 = poker.deal(
0
);
// 发第一张牌
// 对于非静态内部类Card
// 只有通过其外部类Poker对象才能创建Card对象
Poker.Card c2 = poker.
new
Card(
"红心"
,
1
);
// 自己创建一张牌
System.out.println(c1);
// 洗牌后的第一张
System.out.println(c2);
// 打印: 红心A
}
}
面试题 – 下面的代码哪些地方会产生编译错误?
123456789101112class
Outer {
class
Inner {}
public
static
void
foo() {
new
Inner(); }
public
void
bar() {
new
Inner(); }
public
static
void
main(String[] args) {
new
Inner();
}
}
注意:Java中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中foo和main方法都是静态方法,静态方法中没有this,也就是说没有所谓的外部类对象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样做:
1new
Outer().
new
Inner();
- Java 中会存在内存泄漏吗,请简单描述。
答:理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。例如hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。下面例子中的代码也会导致内存泄露。123456789101112131415161718192021222324252627282930import
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。
- 抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被synchronized修饰?
答:都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。 - 阐述静态变量和实例变量的区别。
答:静态变量是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。补充:在Java开发中,上下文类和工具类中通常会有大量的静态成员。
- 如何实现对象克隆?
答:有两种方式:
1). 实现Cloneable接口并重写Object类中的clone()方法;
2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。123456789101112131415161718192021222324import
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();
// 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
// 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
}
}
下面是测试代码:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950import
java.io.Serializable;
/**
* 人类
* @author 骆昊
*
*/
class
Person
implements
Serializable {
private
static
final
long
serialVersionUID = -9102017020286042305L;
private
String name;
// 姓名
private
int
age;
// 年龄
private
Car car;
// 座驾
public
Person(String name,
int
age, Car car) {
this
.name = name;
this
.age = age;
this
.car = car;
}
public
String getName() {
return
name;
}
public
void
setName(String name) {
this
.name = name;
}
public
int
getAge() {
return
age;
}
public
void
setAge(
int
age) {
this
.age = age;
}
public
Car getCar() {
return
car;
}
public
void
setCar(Car car) {
this
.car = car;
}
@Override
public
String toString() {
return
"Person [name="
+ name +
", age="
+ age +
", car="
+ car +
"]"
;
}
}
1234567891011121314151617181920212223242526272829303132333435363738/**
* 小汽车类
* @author 骆昊
*
*/
class
Car
implements
Serializable {
private
static
final
long
serialVersionUID = -5713945027627603702L;
private
String brand;
// 品牌
private
int
maxSpeed;
// 最高时速
public
Car(String brand,
int
maxSpeed) {
this
.brand = brand;
this
.maxSpeed = maxSpeed;
}
public
String getBrand() {
return
brand;
}
public
void
setBrand(String brand) {
this
.brand = brand;
}
public
int
getMaxSpeed() {
return
maxSpeed;
}
public
void
setMaxSpeed(
int
maxSpeed) {
this
.maxSpeed = maxSpeed;
}
@Override
public
String toString() {
return
"Car [brand="
+ brand +
", maxSpeed="
+ maxSpeed +
"]"
;
}
}
12345678910111213141516class
CloneTest {
public
static
void
main(String[] args) {
try
{
Person p1 =
new
Person(
"Hao LUO"
,
33
,
new
Car(
"Benz"
,
300
));
Person p2 = MyUtil.clone(p1);
// 深度克隆
p2.getCar().setBrand(
"BYD"
);
// 修改克隆的Person对象p2关联的汽车对象的品牌属性
// 原来的Person对象p1关联的汽车不会受到任何影响
// 因为在克隆Person对象时其关联的汽车对象也被克隆了
System.out.println(p1);
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是优于把问题留到运行时。
- String s = new String(“xyz”);创建了几个字符串对象?
答:两个对象,一个是静态区的”xyz”,一个是用new创建在堆上的对象。 - 一个”.java”源文件中是否可以包含多个类(不是内部类)?有什么限制?
答:可以,但一个源文件中最多只能有一个公开类(public class)而且文件名必须和公开类的类名完全保持一致。 - 指出下面程序的运行结果。
123456789101112131415161718192021222324252627282930
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。创建对象时构造器的调用顺序是:先初始化静态成员,然后调用父类构造器,再初始化非静态成员,最后调用自身构造器。
提示:如果不能给出此题的正确答案,说明之前第21题Java类加载机制还没有完全理解,赶紧再看看吧。
- 数据类型之间的转换:
- 如何将字符串转换为基本数据类型?
- 如何将基本数据类型转换为字符串?
答:
- 调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)即可返回相应基本类型;
- 一种方法是将基本数据类型与空字符串(”")连接(+)即可获得其所对应的字符串;另一种方法是调用String 类中的valueOf()方法返回相应字符串 - 比较一下Java和JavaSciprt。
答:JavaScript 与Java是两个公司开发的不同的两个产品。Java 是原Sun Microsystems公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;而JavaScript是Netscape公司的产品,为了扩展Netscape浏览器的功能而开发的一种可以嵌入Web页面中运行的基于对象和事件驱动的解释性语言。JavaScript的前身是LiveScript;而Java的前身是Oak语言。
下面对两种语言间的异同作如下比较:
- 基于对象和面向对象:Java是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。
- 解释和编译:Java的源代码在执行之前,必须经过编译。JavaScript是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了JIT(即时编译)技术来提升JavaScript的运行效率)
- 强类型变量和类型弱变量:Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript的解释器在运行时检查推断其数据类型。
- 代码格式不一样。补充:上面列出的四点是网上流传的所谓的标准答案。其实Java和JavaScript最重要的区别是一个是静态语言,一个是动态语言。目前的编程语言的发展趋势是函数式语言和动态语言。在Java中类(class)是一等公民,而JavaScript中函数(function)是一等公民,因此JavaScript支持函数式编程,可以使用Lambda函数和闭包(closure),当然Java 8也开始支持函数式编程,提供了对Lambda表达式以及函数式接口的支持。对于这类问题,在面试的时候最好还是用自己的语言回答会更加靠谱,不要背网上所谓的标准答案。
- try{}里有一个return语句,那么紧跟在这个try后的finally{}里的代码会不会被执行,什么时候被执行,在return前还是后?
答:会执行,在方法返回调用者前执行。注意:在finally中改变返回值的做法是不好的,因为如果存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值,然后如果在finally中修改了返回值,就会返回修改后的值。显然,在finally中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java中也可以通过提升编译器的语法检查级别来产生警告或错误,Eclipse中可以在如图所示的地方进行设置,强烈建议将此项设置为编译错误。
- 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语句或者最终将异常抛给JVM。 - 列出一些你常见的运行时异常?
答:
- ArithmeticException(算术异常)
- ClassCastException (类转换异常)
- IllegalArgumentException (非法参数异常)
- IndexOutOfBoundsException (下标越界异常)
- NullPointerException (空指针异常)
- SecurityException (安全异常) - 阐述final、finally、finalize的区别。
答:
- final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final的方法也同样只能使用,不能在子类中被重写。
- finally:通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
- finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。 - 阐述ArrayList、Vector、LinkedList的存储性能和特性。
答:ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器。LinkedList使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。Vector属于遗留容器(Java早期的版本中提供的容器,除此之外,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也是不正确的。Sun公司的工程师们也会犯这种低级错误,让人唏嘘不已。
- Collection和Collections的区别?
答:Collection是一个接口,它是Set、List等容器的父接口;Collections是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。 - TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
答:TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。
例子1:1234567891011121314151617181920public
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;
// 比较年龄(年龄的升序)
}
}
12345678910111213141516171819202122import
java.util.Set;
import
java.util.TreeSet;
class
Test01 {
public
static
void
main(String[] args) {
Set<Student> set =
new
TreeSet<>();
// Java 7的钻石语法(构造器后面的尖括号中不需要写类型)
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);
}
// 输出结果:
// Student [name=Bob YANG, age=22]
// Student [name=XJ WANG, age=32]
// Student [name=Hao LUO, age=33]
// Student [name=Bruce LEE, age=60]
}
}
例子2:
1234567891011121314151617181920212223242526272829public
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 +
"]"
;
}
}
123456789101112131415161718192021222324252627282930313233343536import
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<>();
// Java 7的钻石语法(构造器后面的尖括号中不需要写类型)
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
));
// 通过sort方法的第二个参数传入一个Comparator接口对象
// 相当于是传入一个比较对象大小的算法到sort方法中
// 由于Java中没有函数指针、仿函数、委托这样的概念
// 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调
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);
}
// 输出结果:
// Student [name=Bob YANG, age=22]
// Student [name=Bruce LEE, age=60]
// Student [name=Hao LUO, age=33]
// Student [name=XJ WANG, age=32]
}
}
- Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。补充:可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线程编程也不是特别理解。简单的说:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占用了更多的CPU资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费CPU时间。时下很时髦的Node.js就采用了单线程异步I/O的工作模式。
- 线程的sleep()方法和yield()方法有什么区别?
答:
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。 - 请说出与线程同步以及线程调度相关的方法。
答:
- wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
- sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
- notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
- notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;提示:关于Java多线程和并发编程的问题,建议大家看我的另一篇文章《关于Java并发编程的总结和思考》。
补充:Java 5通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。
下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。
- 银行账户类:
123456789101112131415161718192021222324252627282930/**
* 银行账户
* @author 骆昊
*
*/
public
class
Account {
private
double
balance;
// 账户余额
/**
* 存款
* @param money 存入金额
*/
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;
}
}
- 存钱线程类:
1234567891011121314151617181920/**
* 存钱线程
* @author 骆昊
*
*/
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);
}
}
- 测试类:
1234567891011121314151617181920import
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)关键字
123456789101112131415161718192021222324252627282930/**
* 银行账户
* @author 骆昊
*
*/
public
class
Account {
private
double
balance;
// 账户余额
/**
* 存款
* @param money 存入金额
*/
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;
}
}
- 在线程调用存款方法时对银行账户进行同步
12345678910111213141516171819202122/**
* 存钱线程
* @author 骆昊
*
*/
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);
}
}
}
- 通过Java 5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
12345678910111213141516171819202122232425262728293031323334353637383940414243import
java.util.concurrent.locks.Lock;
import
java.util.concurrent.locks.ReentrantLock;
/**
* 银行账户
*
* @author 骆昊
*
*/
public
class
Account {
private
Lock accountLock =
new
ReentrantLock();
private
double
balance;
// 账户余额
/**
* 存款
*
* @param money
* 存入金额
*/
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元。当然也可以使用Semaphore或CountdownLatch来实现同步。
- 编写多线程程序有几种实现方式?
答:Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法可以在线程执行结束时产生一个返回值,代码如下所示:
12345678910111213141516171819202122232425262728293031323334353637383940414243import
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;
}
}
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);
}
}
- synchronized关键字的用法?
答:synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。 - 什么是线程池(thread pool)?
答:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
- newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。 - 线程的基本状态以及状态之间的关系?
答:
说明:其中Running表示运行状态,Runnable表示就绪状态(万事俱备,只欠CPU),Blocked表示阻塞状态,阻塞状态又有多种情况,可能是因为调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断。
- 简述synchronized 和java.util.concurrent.locks.Lock的异同?
答:Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。