【Java面试题】Java面试题及答案
2021-11-25 14:32:49
1、string类可以被继承吗?
String类在声明时使用final关键字修饰,被final关键字修饰的类无法被继承。
接下来我们可以看一下String类的源代码片段: public final class String implements java.io.Serializable, Comparable
● 为什么Java语言的开发者,把String类定义为final的呢?
因为只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
● final关键字除了修饰类之外,还有哪些用法呢?
final修饰的变量,一旦赋值,不可重新赋值;
final修饰的方法无法被覆盖;
final修饰的实例变量,必须手动赋值,不能采用系统默认值;
final修饰的实例变量,一般和static联用,用来声明常量;
注意:final不能和abstract关键字联合使用。
总之,final表示最终的、不可变的。 2、& 和 && 的区别?
● &运算符是:逻辑与;&&运算符是:短路与。
● &和&&在程序中最终的运算结果是完全一致的,只不过&&存在短路现象,当&&运算符左边的表达式结果为false的时候,右边的表达式不执行,此时就发生了短路现象。如果是&运算符,那么不管左边的表达式是true还是false,右边表达式是一定会执行的。这就是他们俩的本质区别。
● 当然,&运算符还可以使用在二进制位运算上,例如按位与操作。
3、两个对象值相同equals结果为true,但却可有不同的 hashCode,这句话对不对?
不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希值(hashCode)应当相同。Java 对于equals方法和hashCode方法是这样规定的:
(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;
(2)如果两个对象的 hashCode相同,它们并不一定相同。当然,你未必按照要求去做,但是如果你违背了上述原则就会发现在使用集合时,相同的对象可以出现在Set 集合中,同时增加新元素的效率会大大降低(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
关于equals和hashCode方法,很多java程序员都知道,但很多人也就是仅仅了解而已,在Joshua Bloch的大作《Effective Java》(《Effective 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方法的诀窍包括:
使用==操作符检查"参数是否为这个对象的引用";
使用 instanceof操作符检查"参数是否为正确的类型";
对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;
编写完equals方法后,问自己它是否满足对称性、传递性、一致性;
重写equals时总是要重写hashCode;
不要将equals方法参数中的Object对象替换为其他的类型,在重写时不要忘掉@Override注解。 4、在 Java 中,如何跳出当前的多重嵌套循环?
在最外层循环前加一个标记如outfor,然后用break outfor;可以跳出多重循环。例如以下代码: public class TestBreak { public static void main(String[] args) { outfor: for (int i = 0; i < 10; i++){ for (int j = 0; j < 10; j++){ if (j == 5){ break outfor; } System.out.println("j = " + j); } } } }
运行结果如下所示:
j = 0
j = 1
j = 2
j = 3
j = 4 5、重载(Overload)和重写(override)的区别?重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载发生在一个类中,同名的方法如果有不同的参数列表(类型不同、个数不同、顺序不同)则视为重载。
重写发生在子类与父类之间,重写要求子类重写之后的方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。
● 方法重载的规则:
方法名一致,参数列表中参数的顺序,类型,个数不同。
重载与方法的返回值无关,存在于父类和子类,同类中。
可以抛出不同的异常,可以有不同修饰符。
● 方法重写的规则:
参数列表、方法名、返回值类型必须完全一致;
构造方法不能被重写;
声明为 final 的方法不能被重写;
声明为 static 的方法不存在重写(重写和多态联合才有意义);
访问权限不能比父类更低;
重写之后的方法不能抛出更宽泛的异常; 6、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里是值传递还是引用传递?
是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的内存地址。这个值(内存地址)被传递后,同一个内存地址指向堆内存当中的同一个对象,所以通过哪个引用去操作这个对象,对象的属性都是改变的。 7、为什么方法不能根据返回类型来区分重载?
我们来看以下的代码: public void testMethod(){ doSome(); } public void doSome(){ } public int doSome(){ return 1; }
在Java语言中,调用一个方法,即使这个方法有返回值,我们也可以不接收这个返回值,例如以上两个方法doSome(),在testMethod()中调用的时候,Java编译器无法区分调用的具体是哪个方法。所以对于编译器来说,doSome()方法不是重载而是重复了,编译器报错。所以区分这两个方法不能依靠方法的返回值类型。 8、抽象类(abstract class)和接口(interface)有什么异同?
不同点:
●抽象类中可以定义构造器,接口不能;
●抽象类可以有抽象方法和具体方法,接口不能有具体方法;
●接口中的成员全都是public 的,抽象类中的成员可以使用private、public、protected、默认等修饰;
●抽象类中可以定义成员变量,接口中只能是常量;
●有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法;
●抽象类中可以包含静态方法,接口中不能有静态方法;
●一个类只能继承一个抽象类,一个类可以实现多个接口;
相同点:
●不能够实例化;
●可以将抽象类和接口类型作为引用类型;
●一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类;
9、char 型变量中能不能存储一个中文汉字,为什么?
char 类型可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char 类型占2个字节(16 比特),所以放一个中文是没问题的。
补充:使用Unicode 意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是 Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如 InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务。 10、抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被 synchronized?
都不能。
● 抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。
● 本地方法是由本地代码(如 C++ 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。
● synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。 11、==和equals的区别?
equals和==最大的区别是一个是方法一个是运算符。
● ==:如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
● equals():用来比较方法两个对象的内容是否相等。equals方法不能用于基本数据类型的变量,如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址。 12、阐述静态变量和实例变量的区别?
不管创建多少个对象,静态变量在内存中有且仅有一个;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。 13、break和continue的区别?
● break和continue 都是用来控制循环的语句。
● break 用于完全结束一个循环,跳出循环体执行循环后面的语句。
continue 用于跳过本次循环,继续下次循环。 14、String s = "Hello";s = s + " world!";这两行代码执行后,原始的 String 对象中的内容变了没有?
没有。
因为 String被设计成不可变类,所以它的所有对象都是不可变对象。
在这段代码中,s原先指向一个 String 对象,内容是 "Hello",然后我们对 s 进行了“+”操作,那么 s 所指向的那个对象是否发生了改变呢?
答案是没有。这时s不指向原来那个对象了,而指向了另一个 String 对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是 s 这个引用变量不再指向它了。
通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用 String 来代表字符串的话会引起很大的内存开销。因为 String 对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个 String 对象来表示。这时,应该考虑使用 StringBuffer/StringBuilder类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都 new 一个 String。例如我们要在构造器中对一个名叫 s 的 String 引用变量进行初始化,把它设置为初始值,应当这样做:
s = new String("动力节点,口口相传的Java黄埔军校");
而不是这样做:
s = new String("动力节点,口口相传的Java黄埔军校");
后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为 String 对象不可改变,所以对于内容相同的字符串,只要一个 String 对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的 String 类型属性 s 都指向同一个对象。
上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java 认为它们代表同一个 String 对象。而用关键字 new 调用构造器,总是会创建一个新的对象,无论内容是否相同。
至于为什么要把 String 类设计成不可变类,是它的用途决定的。其实不只String,很多 Java 标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以 Java 标准类库还提供了一个可变版本,即 StringBuffer。