Java 学习笔记(1):牛客找虐记
注意:文章内容均总结于牛客网,解析参照大佬的讲解
一、 HashMap 和 HashTable
1.1 源码
// HashMap的源码
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
// Hashtable的源码
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
// HashMap的put方法,没有同步
public V put(K key, V value)
// Hashtable的put方法,与同步
// 当然,Hashtable的其他方法,如get,size,remove等方法,都加了synchronized关键词同步操作
public synchronized V put(K key, V value)
// HashMap的put方法中,有如下语句
// 调用某个方法直接把key为null,值为value的键值对插入进去。
if (key == null)
return putForNullKey(value);
// Hashtable的put方法有以下语句块,key 不可为 null
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
//以下是HashMap中的方法,注意,没有contains方法,所以,D错误
public boolean containsKey(Object key)
public boolean containsValue(Object value)
//以下是Hashtable的方法
public synchronized boolean contains(Object value)
public synchronized boolean containsKey(Object key)
public boolean containsValue(Object value)
1.2 总结
1.2.1 HashMap
-
HashMap
实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap
的底层结构是一个数组,数组中的每一项是一条链表。 -
HashMap
的实例有俩个参数影响其性能: “初始容量” 和 装填因子。 -
HashMap
实现不同步,线程不安全。HashTable
线程安全 -
HashMap
中的key-value
都是存储在Entry
中的。 -
HashMap
可以存null
键和null
值,不保证元素的顺序恒久不变,它的底层使用的是数组和链表,通过hashCode()
方法和equals
方法保证键的唯一性 -
解决冲突主要有三种方法:定址法,拉链法,再散列法。
HashMap
是采用拉链法解决哈希冲突的。
注: 链表法是将相同hash
值的对象组成一个链表放在hash值对应的槽位;
开放定址法
解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。 沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。
拉链法
解决冲突的做法是: 将所有关键字为同义词的结点链接在同一个单链表中 。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。拉链法适合未规定元素的大小。
1.2.2 Hashtable 和 HashMap 的区别:
-
继承不同:
public class Hashtable extends Dictionary implements Map public class HashMap extends AbstractMap implements Map
-
Hashtable
中的方法是同步的,而HashMap
中的方法在缺省情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable
,但是要使用HashMap
的话就要自己增加同步处理了。 -
HashTable
中,key
和value
都不允许出现 null 值。 在HashMap
中,null
可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null
。当get()
方法返回null
值时,即可以表示HashMap
中没有该键,也可以表示该键所对应的值为null
。因此,在HashMap
中不能由get()
方法来判断HashMap
中是否存在某个键, 而应该用containsKey()
方法来判断。 -
两个遍历方式的内部实现上不同。
Hashtable
、HashMap
都使用了Iterator
。而由于历史原因,Hashtable
还使用了Enumeration
的方式 。 -
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
-
Hashtable
和HashMap
它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable
中hash
数组默认大小是11,增加的方式是old*2+1。HashMap
中hash
数组的默认大小是16,而且一定是2的指数。
注: HashSet
子类依靠hashCode()
和equal()
方法来区分重复元素。
HashSe``t内部使用Map
保存数据,即将HashSet
的数据作为Map
的key
值保存,这也是HashSet
中元素不能重复的原因。而Map
中保存key
值的,会去判断当前Map
中是否含有该Key
对象,内部是先通过key
的hashCode
,确定有相同的hashCode
之后,再通过equals
方法判断是否相同。
二、构造块 和 静态块
2.1 题目
public class B
{
public static B t1 = new B();
public static B t2 = new B();
{
System.out.println("构造块");
}
static
{
System.out.println("静态块");
}
public static void main(String[] args)
{
B t = new B();
}
2.2 解析
输出结果:构造块 构造块 静态块 构造块
-
开始时
JVM
加载B.class
,对所有的静态成员进行声明,t1 t2
被初始化为默认值,为null
,又因为t1 t2
需要被显式初始化,所以对t1
进行显式初始化,初始化代码块→构造函数(没有就是调用默认的构造函数)。 -
静态代码块不初始化:因为在开始时已经对
static
部分进行了初始化,虽然只对static
变量进行了初始化,但在初始化t1
时也不会再执行static
块了。因为JVM
认为这是第二次加载B.class
了,所以static
会在t1
初始化时被忽略掉,直接初始化非static
部分,也就是构造块部分(输出’‘构造块’’)接着构造函数(无输出)。 -
接着对
t2
进行初始化过程同t1
相同(输出’构造块’),此时就对所有的static
变量都完成了初始化,接着就执行static
块部分(输出’静态块’),接着执行,main
方法,同样也,new
了对象,调用构造函数输出(‘构造块’)`
注:并不是静态块最先初始化,而是静态域。
静态域中包含静态变量、静态块和静态方法,其中需要初始化的是静态变量和静态块.而他们两个的初始化顺序是自上而下,自左向右!
三、鲁棒性
3.1 概念
鲁棒性(Robust,即健壮性)
-
Java
在编译和运行程序时,都要对可能出现的问题进行检查,以消除错误的产生。它提供自动垃圾收集来进行内存管理,防止程序员在管理内存时容易产生 的错误。通过集成的面向对象的例外处理机制,在编译时,Java揭示出可能出现但未被处理的例外,帮助程序员正确地进行选择以防止系统的崩溃。 -
另外, Java在编译时还可捕获类型声明中的许多常见错误,防止动态运行时不匹配问题的出现。
3.2 特点
-
Java
在编译和运行程序时都要对可能出现的问题进行检查,以防止错误的产生; -
Java
编译器可以查出许多其他语言运行时才能发现的错误; -
Java
不支持指针操作,大大减少了错误发生的可能性; -
Java
具有异常处理的功能,当程序异常时,它能捕获并响应意外情况,以保证程序能稳妥地结束,计算机系统不会崩溃;
四、 实参 和 形参
4.1 题目
public class Tester{
public static void main(String[] args){
Integer var1=new Integer(1);
Integer var2=var1;
doSomething(var2);
System.out.print(var1.intValue());
System.out.print(var1==var2);
}
public static void doSomething(Integer integer){
integer=new Integer(2);
}
}
运行结果:1true
4.2 解析
java中引用类型的实参向形参的传递,只是传递的引用,而不是传递的对象本身。
五、 try - catch - finally
5.1 题目
package algorithms.com.guan.javajicu;
public class TestDemo
{
public static String output = ””;
public static void foo(inti)
{
try
{
if (i == 1)
{
throw new Exception();
}
}
catch (Exception e)
{
output += “2”;
return ;
} finally
{
output += “3”;
}
output += “4”;
}
public static void main(String[] args)
{
foo(0);
foo(1);
System.out.println(output);
}
}
运行结果:3423
5.2 解析
5.2.1 步骤推演
-
首先是
foo(0)
在try代码块中未抛出异常,finally
是无论是否抛出异常必定执行的语句,所以output += “3”
;然后是output += “4”
; -
执行foo(1)的时候,
try
代码块抛出异常,进入catch
代码块,output += “2”
;
前面说过finally
是必执行的,即使return
也会执行output += “3”
-
由于
catch
代码块中有return
语句,最后一个output += “4”
不会执行。
所以结果是3423
5.2.2 误区
try-catch-finall
y块中,finally
块在以下几种情况将不会执行。
-
finally
块中发生了异常。 -
程序所在线程死亡。
-
在前面的代码中用了
System.exit()
; -
关闭了
CPU
六、多态
6.1题目
class Test {
public static void main(String[] args) {
System.out.println(new B().getValue());
}
static class A {
protected int value;
public A (int v) {
setValue(v);
}
public void setValue(int value) {
this.value= value;
}
public int getValue() {
try {
value ++;
return value;
} finally {
this.setValue(value);
System.out.println(value);
}
}
}
static class B extends A {
public B () {
super(5);
setValue(getValue()- 3);
}
public void setValue(int value) {
super.setValue(2 * value);
}
}
}
运行结构:22 34 17
6.2 解析
6.2.1 多态特性
执行对象实例化过程中遵循多态特性
:
- 调用的方法都是实例化的子类中的重写方法
- 只有明确调用了
super
关键词或者是子类中没有该方法时,才会去调用父类相同的同名方法
6.2.2 步骤推演
Step 1: new B()
构造一个 B 类的实例
- 此时
super(5)
语句显示调用父类 A 带参的构造函数,该构造函数调setValue(v)
。虽然构造函数是 A 类的构造函数,但此刻正在初始化的对象是 B 的一个实例,因此这里调用的实际是 B 类的setValue
方法,于是调用B类中的setValue
方法 。 - 然而,B 类中
setValue
方法显示调用父类的setValue
方法,将 B 实例的value
值设置为 2 x 5 = 10。 - 接着,B类的构造函数还没执行完成,继续执行
setValue(getValue()- 3)
// 备注1 - 先执行
getValue
方法,B 类中没有重写getValue
方法,因此调用父类 A 的getValue
方法。:- 调用
getValue
方法之前,B 的成员变量value
值为10。 value++
执行后, B 的成员变量value
值为11,此时开始执行到return
语句,将11这个值作为getValue
方法的返回值返回出去。- 但是由于
getValue
块被try finally
块包围,因此finally
中的语句无论如何都将被执行,所以步骤 2 中 11 这个返回值会先暂存起来,到finally
语句块执行完毕后再真正返回出去。 - 这里有很重要的一点:
finally
语句块中this.setValue(value)
方法调用的是 B 类的setValue
方法。为什么?因为此刻正在初始化的是 B 类的一个对象(运行时多态),就像最开始第一步提到的一样(而且这里用了使用了this
关键词显式指明了调用当前对象的方法)。因此,此处会再次调用 B 类的setValue
方法,同上,super.
关键词显式调用 A 的setValue
方法,将 B 的value
值设置成为了2 * 11 = 22。 - 因此第一项打印项为 22 。
finally
语句执行完毕 会把刚刚暂存起来的11 返回出去,也就是说这么经历了这么一长串的处理,getValue
方法最终的返回值是11。
回到前面标注了 // 备注1 的代码语句,其最终结果为setValue(11-3) => setValue(8)
。
这里执行的setValue
方法,将会是 B 的setValue
方法。 之后 B 的value
值再次变成了2*8 = 16;
- 调用
Step2:new B().getValue()
B 类中没有独有的getValue
方法,此处调用A的getValue
方法。同Step 1
- 调用
getValue
方法之前,B 的成员变量value
值为16。 value++
执行后, B 的成员变量value
值为17,此时执行到return
语句,会将17这个值作为getValue
方法的返回值返回出去- 但是由于
getValue
块被try finally
块包围而finally
中的语句无论如何都一定会被执行,所以步骤2中17这个返回值会先暂存起来,到finally
语句块执行完毕后再真正返回出去。 finally
语句块中继续和上面说的一样:this.setValue(value)
方法调用的是 B 类的setValue()
方法将 B 的value
值设置成为了2 * 17 = 34。- 因此第二个打印项为34。
finally
语句执行完毕 会把刚刚暂存起来的17返回出去。- 因此
new B().getValue()
最终的返回值是17.
Step3: main => System.out.println
- 将刚刚返回的值打印出来,也就是第三个打印项:17
最终结果为 22 34 17