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-finally块中,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