面试题(一)—Java基础(上)
1.面向对象的三大特征
(1)封装
封装性指的是隐藏了对象的属性和实现细节,对外仅提供公共的访问方式。
好处: 将变化隔离,提供复用性和安全性。
(2)继承
提高代码的复用性,继承是多态的前提。
子类所有的构造方法都会默认访问父类的空参数构造方法,默认第一行有super()。若无空参构造函数,子类需指定,另外子类的构造函数可以用this关键字指定自身的其他构造函数。
(3)多态
父类的引用指向子类对象,提高了程序的扩展性,虽然提高了扩展性,但是只能访问父类具备的方法,不能访问子类中的方法。
2.四种访问权限修饰符
3.Java的基本数据类型
Java四类八种基本数据类型
整型: byte(1)、short(2)、int(4)、long(8)
浮点型: float(4)、double(8)
布尔型: boolean(1/8)
字符型: char(2)
4.short s =1;s = s + 1; 与 short s = 1; s+=1;
由于s+1运算时会自动提升表达式的类型,所以结果是int型,再赋值给short类型s时,编译器将报告需要强制转换类型的错误。
对于short s = 1; s += 1;由于 += 是java语言规定的运算符,java编译器会对它进行特殊处理,因此可以正确编译。
5.&与&&的区别
&和&&都可以用作逻辑与的运算符,表示逻辑与(and)。当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。
&&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式。例如,对于if(str != null && !str.equals(“”))表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。If(x==33 & ++y>0) y会增长,If(x==33 && ++y>0)不会增长。
&还可以用作位运算符。当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。
6.int与Integer的区别
int是基本数据类型,Integer是int的一个包装类。
@Test public void testInteger(){ 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比较 }
@Test public void testInt(){ Integer a = 100; Integer b = 100; Integer c = 200; Integer d = 200; System.out.println(a == b); //true System.out.println(c == d); //false }
当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf。
public static Integer valueOf(int i) { final int offset = 128; if (i >= -128 && i <= 127) { // must cache return IntegerCache.cache[i + offset]; } return new Integer(i); }
如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象。
7.final、finally、finalize的区别
final: final修饰类:当一个类被final修饰时,表示该类是一个终态类,即不能被继承。
final 修饰方法:当一个方法被final修饰时,此方法不能被重写(Override)。
final 修饰属性:当一个属性被final修饰时,此属性不能被改写。
当final修饰一个原生数据类型时,表示该原生数据类型的值不能发生变化;
当final修饰一个引用类型时,表示该引用类型不能再指向其他对象了,但该引用所指向的内容是可以发生
变化的。
其初始化可以在两个地方:
(1)定义处,在final变量定义时直接给其赋值;
(2)构造函数中。
finally: 异常处理语句结构的一部分,表示总是执行。
finalize: 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。 JVM 不保证此方法总被调用。
8."=="和"equals"
"=="既可以用于基本数据类型的比较,也可以用于引用类型的比较。
"equals"只能用于引用类型的比较。
"=="
(1)对于原生类型来说,比较的是左右两边的值是否相等。
(2)对于引用类型来说,比较左右两边的引用是否指向同一个对象,或者说左右两边的引用地址是否相同。
equals()方法
该方法定义在Object类中,因此Java每个类中都具有该方法,对于Object类的equals()方法来说,它判断的是用equals()方法与传进来的引用是否一致,即两个引用是否指向同一个对象。对于Object类的equals()方法,等价于==。对于String类中equals()方法来说,它复写了Object类中的equals()方法,它是判断当前字符串与传进来的字符串的内容是否一致。
9.Overload和Override
方法重载Overload
表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同。
方法参数不同有两层含义:
(1)参数个数不同。
(2)参数类型不同。 注意:方法的返回值对重载没有任何影响。
重写Override
表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了。
这也是面向对象编程的多态性的一种表现。子类覆盖父类的方法时, 只能比父类抛出更少的异常,或者是抛出父类抛出的异常的子异常,因为子类可以解决父类的一些问题,不能比父类有更多的问题。子类方法的访问权限只能比父类的更大,不能更小。如果父类的方法是 private 类型,那么,子类则不存在覆盖的限制,相当于子类中增加了一个全新的方法。
10.Abstract Class和interface的区别
Abstract Class
含有abstract修饰符的class即为抽象类, abstract类不能创建的实例对象。含有abstract方法的类必须定义为 abstract class, 抽象类中的方法不必是抽象的。抽象类中定义抽象方法必须在具体子类中实现, 所以,不能有抽象构造方法或抽象静态方法。 如果的子类没有实现抽象父类中的所有抽象方法,那么子类也必须定义为 abstract类型。
Interface
可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。 接口中的方法定义默认为 public abstract 类型,接口中的成员变量类型默认为 public static final。
A: 抽象类只能被单继承。
接口可以多实现,接口的出现避免了多继承的局限性。
B: 抽象类中的数据特点
成员变量: 可以是变量,也可以是常量。
成员方法: 可以是抽象方法,也可以是非抽象方法。
构造方法: 有构造方法。
接口中的数据特点
成员变量: 是常量。默认修饰 public static final。
成员方法: 都是抽象方法。都有默认修饰 public abstract。
构造方法: 没有构造方法。
C: 抽象类中定义的是继承体系中的共性功能。
接口中定义的是继承体系中的扩展功能。
11.String、StringBuffer、StringBuilder的区别
String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)
执行速度: StringBuilder > StringBuffer>String
简要的说,String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。
而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。
而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
String S1 = “This is only a” + “ simple” + “ test”; StringBuffer Sb = new StringBuilder(“This is only a”).append(“simple”).append(“ test”);
你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个
String S1 = “This is only a” + “ simple” + “test”; 其实就是:
String S1 = “This is only a simple test”;
所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:
String S2 = “This is only a”; String S3 = “ simple”; String S4 = “ test”; String S1 = S2 +S3 + S4;
这时候 JVM 会规规矩矩的按照原来的方式去做。
12.try {}里有一个 return 语句,那么 紧跟在这个 try 后的 finally {}里的 code会不会被执行?
public static int testDemo(){ int i = 1; try{ System.out.println("Before retrun..."); return i; } finally{ System.out.println("finally invoked..."); ++i; } }
输出:
Before retrun...
finally invoked...
1
在try语句中,在执行return语句时,要返回的结果已经准备好了,就在此时,程序转到finally执行了。在转去之前,try中先把要返回的结果存放到不同于i的局部变量中去,执行完finally之后,在从中取出返回结果,因此,即使finally中对变量i进行了改变,但是不会影响返回结果。
13.List子类的特点
ArrayList
底层数据结构是数组,查询快,增删慢。
线程不安全,效率高。
Vector
底层数据结构是数组,查询快,增删慢。
线程安全,效率低。
LinkedList
底层数据结构是链表,查询慢,增删快。
线程不安全,效率高。
14.ArrayList和Vector的区别
(1)同步性
Vector 是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList 是线程序不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
(2)数据增长
ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加 ArrayList 与 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。 Vector 默认增长为原来两倍,而ArrayList 是增长为原来的1.5倍。 ArrayList 与 Vector 都可以设置初始的空间大小, Vector 还可以设置增长的空间大小,而 ArrayList 没有提供设置增长空间的方法。
总结:即 Vector 增长原来的一倍, ArrayList 增加原来的0.5倍。
15.List、Set、Map的区别
List、Set都继承自Collection接口。Map没有。
List:有序的,元素可重复,有索引。
Set:无序的,元素不可重复,无索引。
Map:元素按键值对存储,无存放顺序。
16.HashMap和Hashtable的区别
(1)继承和实现方式不同
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
(2)线程安全不同
Hashtable的几乎所有函数都是同步的,即它是线程安全的,支持多线程。
HashMap的函数则是非同步的,它不是线程安全的。若要在多线程中使用HashMap,需要我们额外的进行同步处理。 对HashMap的同步处理可以使用Collections类提供的synchronizedMap静态方法,或者直接使用JDK 5.0之后提供的java.util.concurrent包里的ConcurrentHashMap类。
(3)对null值的处理不同
HashMap的key、value都可以为null。
Hashtable的key、value都不可以为null。
Hashtable的key或value,都不能为null!否则,会抛出异常NullPointerException。
HashMap的key、value都可以为null。 当HashMap的key为null时,HashMap会将其固定的插入table[0]位置(即HashMap散列表的第一个位置);而且table[0]处只会容纳一个key为null的值,当有多个key为null的值插入的时候,table[0]会保留最后插入的value。
(4)支持的遍历种类不同
HashMap支持Iterator(迭代器)遍历。
Hashtable支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历。
Enumeration 是JDK 1.0添加的接口,只有hasMoreElements(), nextElement() 两个API接口,不能通过Enumeration()对元素进行修改 。
Iterator 是JDK 1.2才添加的接口,支持hasNext(), next(), remove() 三个API接口。HashMap也是JDK 1.2版本才添加的,所以用Iterator取代Enumeration,HashMap只支持Iterator遍历。
(5)容量的初始值和增加方式都不一样
HashMap默认的容量大小是16;增加容量时,每次将容量变为“原始容量x2”。
Hashtable默认的容量大小是11;增加容量时,每次将容量变为“原始容量x2 + 1”。
17.HashMap的实现原理
HashMap的底层是数组和链表的结合体。
从上图可以看出HashMap底层就是一个数组结构,数组中的每一项又是一个链表,当新建一个HashMap时,就会初始化一个数组。
/** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; …… }
(1)存储
public V put(K key, V value) { // HashMap允许存放null键和null值。 // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。 if (key == null) return putForNullKey(value); // 根据key的keyCode重新计算hash值。 int hash = hash(key.hashCode()); // 搜索指定hash值在对应table中的索引。 int i = indexFor(hash, table.length); // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果i索引处的Entry为null,表明此处还没有Entry。 modCount++; // 将key、value添加到i索引处。 addEntry(hash, key, value, i); return null; }
当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。
(2)读取
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。
HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
18.I/O流的分类
字节流
InputStream
FileInputStream
BufferedInputStream
OutputStream
FileOutputStream
BufferedOutputStream
字符流
Reader
FileReader
BufferedReader
Writer
FileWriter
BufferedWriter
转换流: 转换流的作用就是把字节流转换字符流来使用.
A:OutputStreamWriter
OutputStreamWriter(OutputStream os):默认编码,GBK
OutputStreamWriter(OutputStream os,String charsetName):指定编码。
B:InputStreamReader
InputStreamReader(InputStream is):默认编码,GBK
InputStreamReader(InputStream is,String charsetName):指定编码