示例代码如下
class 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比较
}
}
最近还遇到一个面试题 也是和自动装箱和拆箱有点关系的 代码如下所示
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的内部类 其代码如下所示
/**
* 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
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。
提醒 越是貌似简单的面试题其中的玄机就越多 需要面试者有相当深厚的功力。
8、 和 的区别答 运算符有两种用法 (1)按位与 (2)逻辑与。 运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的 虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。 之所以称为短路运算是因为 如果 左边的表达式的值是false 右边的表达式会被直接短路掉 不会进行运算。很多时候我们可能都需要用 而不是 例如在验证用户登录时判定用户名不是null而且不是空字符串 应当写为 username ! null !username.equals( ) 二者的顺序不能交换 更不能用 运算符 因为第一个条件如果不成立 根本不能进行字符串的equals比较 否则会产生NullPointerException异常。注意 逻辑或运算符 | 和短路或运算符 || 的差别也是如此。
补充 如果你熟悉JavaScript 那你可能更能感受到短路运算的强大 想成为JavaScript的高手就先从玩转短路运算开始吧。
9、解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法。答 通常我们定义一个基本数据类型的变量 一个对象的引用 还有就是函数调用的现场保存都使用JVM中的栈空间 而通过new关键字和构造器创建的对象则放在堆空间 堆是垃圾收集器管理的主要区域 由于现在的垃圾收集器都采用分代收集算法 所以堆空间还可以细分为新生代和老生代 再具体一点可以分为Eden、Survivor 又可分为From Survivor和To Survivor 、Tenured 方法区和堆都是各个线程共享的内存区域 用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据 程序中的字面量 literal 如直接书写的100、 hello 和常量都是放在常量池中 常量池是方法区的一部分 。栈空间操作起来最快但是栈很小 通常大量的对象都是放在堆空间 栈和堆的大小都可以通过JVM的启动参数来进行调整 栈空间用光了会引发StackOverflowError 而堆和常量池空间不足则会引发OutOfMemoryError。
String str new String( hello );
上面的语句中变量str放在栈上 用new创建出来的字符串对象放在堆上 而 hello 这个字面量是放在方法区的。
补充1 较新版本的Java 从Java 6的某个更新开始 中 由于JIT编译器的发展和 逃逸分析 技术的逐渐成熟 栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。
补充2 运行时常量池相当于Class文件常量池具有动态性 Java语言并不要求常量一定只有编译期间才能产生 运行期间也可以将新的常量放入池中 String类的intern()方法就是这样的。
看看下面代码的执行结果是什么并且比较一下jdk7以前和以后的运行结果是否一致。
String.intern()是一个Native方法 它的作用是 如果字符串常量池中已经包含了一个等于此String对象的字符串 则返回代表池(运行时常量池)中这个字符串的String对象 否则 将此String对象包含的字符串添加到常量池中并且返回此String对象的引用。此方法在jdk1.6和jdk1.7中有差异。
String s1 new StringBuilder( go ).append( od ).toString();
System.out.println(s1.intern() s1);
String s2 new StringBuilder( ja ).append( va ).toString();
System.out.println(s2.intern() s2);
这段代码在jdk1.6中运行 会得到两个false 而在jdk1.7中运行会得到一个true一个false。产生差异的原因是 在jdk1.6中 intern()方法会把首次遇到的字符串实例复制到永久代中 返回的也是永久代中这个字符串实例的引用 而用StringBuilder创建的字符串实例在Java堆上 所以必然不是同一个引用 将返回false。而jdk1.7中的intern()实现不会再复制实例 只是在常量池中记录首次出现的实例引用 因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对比str2返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过 字符串常量池中已经有它的引用了 不符合首次出现的原则 而“good”这个字符串则是首次出现的 因此返回true。
现在的疑问是“java”这个字符串在常量池中什么时候存在了
我最开始的猜想是“java”这个字符串是不是常驻在常量池中的 那为什么常驻在常量池中呢 Java虚拟机什么时候加载了“java”这个字符串
答 java虚拟机会自动调用System类 代码如下
/* register the natives via the static initializer.
*
* VM will invoke the initializeSystemClass method to complete
* the initialization for this class separated from clinit.
* Note that to use properties set by the VM, see the constraints
* described in the initializeSystemClass method.
*/
在System类中的注释可以知道 调用了initializeSystemClass方法 在此方法中调用了Version对象的init静态方法
sun.misc.Version.init();
因此sun.misc.Version类会在JDK类库的初始化过程中被加载并初始化。
查看Version类定义的私有静态字符串常量如下
private static final String launcher_name java ;
private static final String java_version 1.7.0_51 ;
private static final String java_runtime_name Java(TM) SE Runtime Environment ;
private static final String java_runtime_version 1.7.0_51-b13 ;
在初始化Version类时 对其静态常量字段根据指定的常量值做默认初始化 所以 java 被加载到了字符串常量池中 修改上面代码使字符串值为上面常量中的任意一个都会返回false。
String str2 new StringBuilder( 1.7.0 ).append( _51 ).toString();
System.out.println(str2.intern() str2);
这个问题解决了 上面这些在虚拟机加载时就初始化的常量 我们再定义其他的字符串常量试试 比如“xiaoyi and heize” 运行结果 true 可以知道 xiaoyi and heize 这个字符串常量没有被预先加载到常量池中。
ps 在虚拟机上进行开发的开发人员称方法区为“永久代” 但两者本质上并不等价 仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集器扩展至方法区 或者说使用永久代来实现方法区而已 但现在看来使用永久代实现方法区并不是一个好主意 因为这样更容易遇到内存溢出问题 在jdk1.7中的HotSpot中 已经把原本放在永久代中的字符串常量池移除---摘自《深入理解Java虚拟机》
10、Math.round(11.5) 等于多少 Math.round(-11.5)等于多少答 Math.round(11.5)的返回值是12 Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。
Math类的成员方法11、switch是否能作用在byte上 是否能作用在long上 是否能作用在String上
public static int round(float a) 四舍五入(参数为double的自学)
要深刻理解四舍五入的具体含义
满足五入的条件后 得到的值要比原来的值大
满足四舍的条件后 得到的值要比原来的值小
不管是正数还是负数。
答 在Java 5以前 switch(expr)中 expr只能是byte、short、char、int 从Java 5开始 Java中引入了枚举类型 expr也可以是enum类型 从Java 7开始 expr还可以是字符串 String 但是长整型 long 在目前所有的版本中都是不可以的。
12、用最有效率的方法计算2乘以8答 2 3 左移3位相当于乘以2的3次方 右移3位相当于除以2的3次方 。
补充 我们为编写的类重写hashCode方法时 可能会看到如下所示的代码 其实我们不太理解为什么要使用这样的乘法运算来产生哈希码 散列码 而且为什么这个数是个素数 为什么通常选择31这个数 前两个问题的答案你可以自己百度一下 选择31是因为可以用移位和减法运算来代替乘法 从而得到更好的性能。说到这里你可能已经想到了 31 * num 等价于(num 5) - num 左移5位相当于乘以2的5次方再减去自身就相当于乘以31 现在的VM都能自动完成这个优化。
为编写的类重写hashCode方法
public class PhoneNumber {13、数组有没有length()方法 String有没有length()方法
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;
}
}
答 数组没有length()方法 有length的属性。
String有length()方法。在JavaScript中 获得字符串的长度是通过length属性得到的 这一点容易和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注解。
答 String 类是final类 不可以被继承。
补充 继承String本身就是一个错误的行为 对String类型最好的重用方式是关联关系 Has-A 和依赖关系 Use-A 而不是继承关系 Is-A 。
18、当一个对象被当作参数传递到一个方法后 此方法可改变这个对象的属性 并可返回变化后的结果 那么这里到底是值传递还是引用传递答 是值传递。Java语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时 参数的值就是对该对象的引用 地址值 。对象的属性可以在被调用过程中被改变 但对对象引用的改变是不会影响到调用者的。C 和C#中可以通过传引用或传输出参数来改变传入的参数的值。在C#中可以编写如下所示的代码 但是在Java中却做不到。
using System;
namespace CS01 {
class Program {
public static void swap(ref int x, ref int y) {
int temp
x
y temp;
}
public static void Main (string[] args) {
int a 5, b 10;
swap (ref a, ref b);
// a 10, b
Console.WriteLine ( a {0}, b {1} , a, b);
}
}
}
说明 Java中没有传引用实在是非常的不方便 这一点在Java 8中仍然没有得到改进 正是如此在Java编写的代码中才会出现大量的Wrapper类 将需要通过方法调用修改的引用置于一个Wrapper类中 再将Wrapper对象传入方法 这样的做法只会让代码变得臃肿 尤其是让从C和C 转型为Java程序员的开发者无法容忍。
其实还是不够明白 我们来看看下面的例子吧
强烈推荐鄙人的例子 参考链接 https://www.cnblogs.com/chenmingjun/p/8698719.html
答 Java平台提供了两种类型的字符串 String和StringBuffer/StringBuilder 它们可以储存和操作字符串。其中String是只读字符串 也就意味着String引用的字符串内容是不能被改变的。而StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder是Java 5中引入的 它和StringBuffer的方法完全相同 区别在于它是在单线程环境下使用的 因为它的所有方面都没有被synchronized修饰 非同步 因此它的效率也比StringBuffer要高。
面试题1 什么情况下用 运算符进行字符串连接比调用StringBuffer/StringBuilder对象的append方法连接字符串性能更好
面试题2 请说出下面程序的输出。
class StringEqualTest {
public static void main(String[] args) {
String s1 Programming ;
String s2 new String( Programming );
String s3 Program ;
String s4 ming ;
String s5 Program ming ;
String s6 s3 s4;
System.out.println(s1 s2); // false
System.out.println(s1 s5); // true
System.out.println(s1 s6); // false
System.out.println(s1 s6.intern()); // true
System.out.println(s2 s2.intern()); // false
}
}
补充 解答上面的面试题需要清除两点
String对象的intern方法会得到字符串对象在常量池中对应的版本的引用 如果常量池中有一个字符串与String对象的equals结果是true 如果常量池中没有对应的字符串 则该字符串将被添加到常量池中 然后返回常量池中字符串的引用。字符串的 操作其本质是创建了StringBuilder对象进行append操作 然后将拼接后的StringBuilder对象用toString方法处理成String对象 这一点可以用javap -c StringEqualTest.class命令获得class文件对应的JVM字节码指令就可以看出来。要想获取对象的内存地址应使用System.identityHashCode()方法。20、重载 Overload 和重写 Override 的区别。重载的方法能否根据返回类型进行区分答 方法的重载和重写都是实现多态的方式 区别在于前者实现的是编译时的多态性 而后者实现的是运行时的多态性。重载发生在一个类中 同名的方法如果有不同的参数列表 参数类型不同、参数个数不同或者二者都不同 则视为重载 重写发生在子类与父类之间 重写要求子类被重写方法与父类被重写方法有相同的返回类型 比父类被重写方法更好访问 不能比父类被重写方法声明更多的异常 里氏代换原则 。重载对返回类型没有特殊的要求。
面试题 华为的面试题中曾经问过这样一个问题 为什么不能根据返回类型来区分重载 快说出你的答案吧
21、描述一下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所指定的目录中记载类 是用户自定义加载器的默认父加载器。
答 char类型可以存储一个中文汉字 因为Java中使用的编码是Unicode 不选择任何特定的编码 直接使用字符在字符集中的编号 这是统一的唯一方法 一个char类型占2个字节 16比特 所以放一个中文是没问题的。
补充 使用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 的内部类 它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化 其语法看起来挺诡异的。
面试题 下面的代码哪些地方会产生编译错误
class 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 也就是说没有所谓的外部类对象 因此无法创建内部类对象 如果要在静态方法中创建内部类对象 可以这样做 new Outer().new Inner();
25、Java中会存在内存泄漏吗 请简单描述。答 理论上Java因为有垃圾回收机制 GC 不会存在内存泄露问题 这也是Java被广泛使用于服务器端编程的一个重要原因 然而在实际开发中 可能会存在无用但可达的对象 这些对象不能被GC回收 因此也会导致内存泄露的发生。例如Hibernate的Session 一级缓存 中的对象属于持久态 垃圾回收器是不会回收这些对象的 然而这些对象中可能存在无用的垃圾对象 如果不及时关闭 close 或清空 flush 一级缓存就可能导致内存泄露。下面例子中的代码也会导致内存泄露。
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接口 通过对象的序列化和反序列化实现克隆 可以实现真正的深度克隆。
注意 基于序列化和反序列化实现的克隆不仅仅是深度克隆 更重要的是通过泛型限定 可以检查出要克隆的对象是否支持序列化 这项检查是编译器完成的 不是在运行时抛出异常 这种是方案明显优于使用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: PrintGCDetails -- 打印GC的细节-XX: PrintGCDateStamps -- 打印GC操作的时间戳-XX:NewSize -- 设置新生代大小-XX:MaxNewSize -- 设置新生代最大大小-XX:NewRatio -- 可以设置老生代和新生代的比例-XX:PrintTenuringDistribution -- 设置每次新生代GC后输出幸存者乐园中对象年龄的分布-XX:InitialTenuringThreshold -- 设置老年代阀值的初始值-XX:MaxTenuringThreshold -- 设置老年代阀值的最大值-XX:TargetSurvivorRatio -- 设置幸存区的目标使用率 31、String s new String( xyz 创建了几个字符串对象答 两个对象 一个是静态区的 xyz 一个是用new创建在堆上的对象。
32、接口是否可继承 extends 接口 抽象类是否可实现 implements 接口 抽象类是否可继承具体类 concrete class答 接口可以继承接口 而且支持多重继承。抽象类可以实现(implements)接口 抽象类可继承具体类也可以继承抽象类。
33、一个 .java 源文件中是否可以包含多个类 不是内部类 有什么限制答 可以 但一个源文件中最多只能有一个公开类 public class 而且文件名必须和公开类的类名完全保持一致。
34、Anonymous Inner Class(匿名内部类)是否可以继承其它类 是否可以实现接口答 可以继承其他类或实现其他接口 在Swing编程和Android开发中常用此方式来实现事件监听和回调。
35、内部类可以引用它的包含类 外部类 的成员吗 有没有什么限制答 一个内部类对象可以访问创建它的外部类对象的成员 包括私有成员。
36、Java 中的final关键字有哪些用法答
(1)修饰类 表示该类不能被继承
(2)修饰方法 表示方法不能被重写
(3)修饰变量 表示变量只能一次赋值以后值不能被修改 常量 。
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类加载机制还没有完全理解 赶紧再看看吧。
38、数据类型之间的转换如何将字符串转换为基本数据类型
答 调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)即可返回相应基本数据类型。
如何将基本数据类型转换为字符串
答 一种方法是将基本数据类型与空字符串 连接 即可获得其所对应的字符串 另一种方法是调用String 类中的valueOf()方法返回相应字符串。
答 方法很多 可以自己写实现也可以使用String或StringBuffer/StringBuilder中的方法。有一道很常见的面试题是用递归实现字符串反转 代码如下所示
public static String reverse(String originStr) {40、怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串
if (originStr null || originStr.length() 1)
return originStr;
return reverse(originStr.substring(1)) originStr.charAt(0);
}
答 代码如下所示
String s1 你好 ;41、日期和时间 如何取得年月日、小时分钟秒 如何取得从1970年1月1日0时0分0秒到现在的毫秒数 如何取得某月的最后一天 如何格式化日期
String s2 new String(s1.getBytes( GB2312 ), ISO-8859-1 );
答
问题1 创建java.util.Calendar实例 调用其get()方法传入不同的参数即可获得参数所对应的值。
Java 8中可以使用java.time.LocalDateTimel来获取 代码如下所示。
public class DateTimeTest {
public static void main(String[] args) {
Calendar cal Calendar.getInstance();
System.out.println(cal.get(Calendar.YEAR));
System.out.println(cal.get(Calendar.MONTH)); // 0 - 11
System.out.println(cal.get(Calendar.DATE));
System.out.println(cal.get(Calendar.HOUR_OF_DAY));
System.out.println(cal.get(Calendar.MINUTE));
System.out.println(cal.get(Calendar.SECOND));
// Java 8
LocalDateTime dt LocalDateTime.now();
System.out.println(dt.getYear());
System.out.println(dt.getMonthValue()); // 1 - 12
System.out.println(dt.getDayOfMonth());
System.out.println(dt.getHour());
System.out.println(dt.getMinute());
System.out.println(dt.getSecond());
}
}
问题2 以下方法均可获得该毫秒数。
Calendar.getInstance().getTimeInMillis();
System.currentTimeMillis();
Clock.systemDefaultZone().millis(); // Java 8
问题3 代码如下所示。
Calendar time Calendar.getInstance();
time.getActualMaximum(Calendar.DAY_OF_MONTH);
问题4 利用java.text.DataFormat 的子类 如SimpleDateFormat类 中的format(Date)方法可将日期格式化。Java 8中可以用java.time.format.DateTimeFormatter来格式化时间日期 代码如下所示。
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
class DateFormatTest {
public static void main(String[] args) {
SimpleDateFormat oldFormatter new SimpleDateFormat( yyyy/MM/dd );
Date date1 new Date();
System.out.println(oldFormatter.format(date1));
// Java 8
DateTimeFormatter newFormatter DateTimeFormatter.ofPattern( yyyy/MM/dd );
LocalDate date2 LocalDate.now();
System.out.println(date2.format(newFormatter));
}
}
补充 Java的时间日期API一直以来都是被诟病的东西 为了解决这一问题 Java 8中引入了新的时间日期API 其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等类 这些的类的设计都使用了不变模式 因此是线程安全的设计。如果不理解这些内容 可以参考我的另一篇文章《关于Java并发编程的总结和思考》。
42、打印昨天的当前时刻。import java.util.Calendar;
class YesterdayCurrent {
public static void main(String[] args){
Calendar cal Calendar.getInstance();
cal.add(Calendar.DATE, -1);
System.out.println(cal.getTime());
}
}
在Java 8中 可以用下面的代码实现相同的功能。
import java.time.LocalDateTime;43、比较一下Java和JavaSciprt。
class YesterdayCurrent {
public static void main(String[] args) {
LocalDateTime today LocalDateTime.now();
LocalDateTime yesterday today.minusDays(1);
System.out.println(yesterday);
}
}
答 JavaScript 与Java是两个公司开发的不同的两个产品。Java 是原Sun Microsystems公司推出的面向对象的程序设计语言 特别适合于互联网应用程序开发 而JavaScript是Netscape公司的产品 为了扩展Netscape浏览器的功能而开发的一种可以嵌入Web页面中运行的基于对象和事件驱动的解释性语言。JavaScript的前身是LiveScript 而Java的前身是Oak语言。
下面对两种语言间的异同作如下比较
补充 上面列出的四点是网上流传的所谓的标准答案。其实Java和JavaScript最重要的区别是一个是静态语言 一个是动态语言。目前的编程语言的发展趋势是函数式语言和动态语言。在Java中类 class 是一等公民 而JavaScript中函数 function 是一等公民 因此JavaScript支持函数式编程 可以使用Lambda函数和闭包 closure 当然Java 8也开始支持函数式编程 提供了对Lambda表达式以及函数式接口的支持。对于这类问题 在面试的时候最好还是用自己的语言回答会更加靠谱 不要背网上所谓的标准答案。
44、什么时候用断言 assert答 断言在软件开发中是一种常用的调试方式 很多开发语言中都支持这种机制。一般来说 断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试时开启。为了保证程序的执行效率 在软件发布后断言检查通常是关闭的。断言是一个包含布尔表达式的语句 在执行这个语句时假定该表达式为true 如果表达式的值为false 那么系统会报告一个AssertionError。断言的使用如下面的代码所示
assert(a // throws an AssertionError if a 0
断言可以有两种形式
assert Expression1;
assert Expression1 : Expression2 ;
Expression1 应该总是产生一个布尔值。
Expression2 可以是得出一个值的任意表达式 这个值用于生成显示更多调试信息的字符串消息。
要在运行时启用断言 可以在启动JVM时使用-enableassertions或者-ea标记。要在运行时选择禁用断言 可以在启动JVM时使用-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。如果写了不能迅速收敛的递归 则很有可能引发栈溢出的错误 如下所示
class StackOverflowErrorTest {
public static void main(String[] args) {
main(null);
}
}
提示 用递归编写程序时一定要牢记两点 1. 递归公式 2. 收敛条件 什么时候就不再继续递归 。
46、try{}里有一个return语句 那么紧跟在这个try后的finally{}里的代码会不会被执行 什么时候被执行 在return前还是后答 会执行 在方法返回调用者前执行。
注意 在finally中改变返回值的做法是不好的 因为如果存在finally代码块 try中的return语句不会立马返回调用者 而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值 然后如果在finally中修改了返回值 就会返回修改后的值。显然 在finally中返回或者修改返回值会对程序造成很大的困扰 C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情 Java中也可以通过提升编译器的语法检查级别来产生警告或错误 Eclipse中可以在如图所示的地方进行设置 强烈建议将此项设置为编译错误。
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语句或者最终将异常抛给JVM。
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()方法可以整理系统资源或者执行其他清理工作。 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 A extends Exception {}
class B extends A {}
public class Test {
public static void main(String[] args) throws Exception {
try {
try {
throw new B();
}
catch (A a) {
System.out.println( Caught A );
throw a;
}
}
catch (B b) {
System.out.println( Caught B );
return ;
}
finally {
System.out.println( Hello World! );
}
}
}
程序输出结果为
Caught A52、List、Set、Map是否继承自Collection接口
Caught B
Hello World!
答 List、Set 是 Map 不是。Map是键值对映射容器 与List和Set有明显的区别 而Set存储的零散的元素且不允许有重复元素 数学中的集合也是如此 List是线性结构的容器 适用于按数值索引访问元素的情形。
53、阐述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公司的工程师们也会犯这种低级错误 让人唏嘘不已。
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方法实现元素的比较 相当于一个临时定义的排序规则 其实就是通过接口注入比较元素大小的算法 也是对回调模式的应用 Java中对函数式编程的支持 。
57、Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行 它们有什么区别答 sleep()方法 休眠 是线程类 Thread 的静态方法 调用此方法会让当前线程暂停执行指定的时间 将执行机会 CPU 让给其他线程 但是对象的锁依然保持 因此休眠时间结束后会自动恢复 线程回到就绪状态 请参考第66题中的线程状态转换图 。wait()是Object类的方法 调用对象的wait()方法导致当前线程放弃对象的锁 线程暂停执行 进入对象的等待池 wait pool 只有调用对象的notify()方法 或notifyAll()方法 时才能唤醒等待池中的线程进入等锁池 lock pool 如果线程重新获得对象的锁就可以进入就绪状态。
补充 可能不少人对什么是进程 什么是线程还比较模糊 对于为什么需要多线程编程也不是特别理解。简单的说 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动 是操作系统进行资源分配和调度的一个独立单位 线程是进程的一个实体 是CPU调度和分派的基本单位 是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程 这使得多线程程序的并发性高 进程在执行时通常拥有独立的内存单元 而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验 但是多线程的程序对于其他程序是不友好的 因为它可能占用了更多的CPU资源。当然 也不是线程越多 程序的性能就越好 因为线程之间的调度和切换也会浪费CPU时间。时下很时髦的Node.js就采用了单线程异步I/O的工作模式。
58、线程的sleep()方法和yield()方法有什么区别答
1、sleep()方法给其他线程运行机会时不考虑线程的优先级 因此会给低优先级的线程以运行的机会 yield()方法只会给相同优先级或更高优先级的线程以运行的机会 2、 线程执行sleep()方法后转入阻塞 blocked 状态 而执行yield()方法后转入就绪 ready 状态 3、sleep()方法声明抛出InterruptedException 而yield()方法没有声明任何异常 4、sleep()方法比yield()方法 跟操作系统CPU调度相关 具有更好的可移植性。 59、当一个线程进入一个对象的synchronized方法A之后 其它线程是否可进入此对象的synchronized方法B答 不能。其它线程只能访问该对象的非同步方法 同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁 如果已经进入A方法说明对象锁已经被取走 那么试图进入B方法的线程就只能在等锁池 注意不是等待池哦 中等待对象的锁。
本文链接: http://ccollectionsb2b.immuno-online.com/view-749206.html