2018-10-12 HashMap、浮点数、类初始化等个人思考

求一个比给定整数大且最接近的2的幂次方整数

tableSizeFor(3)=4,tableSizeFor(14)=6

可以使用位运算。

//学习: 与类成员无交互的方法应该定义为static的
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

解释:其实很好理解,首先需要知道的一点是

2^n-1 的有效比特位全是1
比如:3 二进制11,7二进制111, 15 二进制 1111 …

上面算法也是利用了这一点。直接求出cap-1的有效比特位全是1的数即可。
而右移算法,主要是将最高为1的一直向右复制,因为我们唯一能确定的就是最高位肯定为1.
比如:
10000 右移位:11000,移动后就能确定最高两位一定都是1,
再右移位:11110,变成最高四位一定都是1,依次类推,一共将32位全部覆盖。
最后再加1得到2的整数次幂。

Float.isNaN()方法

有时候传入一个float或者double参数的时候不在运行的时候是无法知道他是不是一个有效的float数值,比如

 float a=0;
 float b=0;
 System.out.println(a/b);

此时不会报错而是输出一个NaN
这种错误一般不在真正使用这个数字是不会报错的。
而有时候一个程序需要的是一个真正有效数字,因此可以使用

Float.isNaN();
Double.isNaN();

方法提前进行判断是否为一个真正有效的数字,将错误进行实时定位。

isNaN()的源码很简单: return (v != v);
因为NaN是唯一一个自己不等于自己的数。
System.out.println(0/0==0/0);//false

基本数据类型

jvm 规范只规定了基本数据类型应该有的有效范围,但是具体在JVM中占多大的内存,并没有规定。
比如 byte只能表示-128-127
但是在JVM中 byte占用多大字节是没有规定的,一般JVM都使用int为标准,也就是还是4字节的
在HotSpot VM 中,char在栈上是占用4字节的,在堆上是占用2字节的
相关连接讨论:char在JVM中到底占用几个字节

浮点数

浮点数是相对于定点数来说的,浮点数就是小数点浮动的小数。
形似如下:

(+ or -)1.(mantissa)*2^exponent
  • mantissa 称为尾数
  • exponent 称为指数
  • float:
    1bit(符号位) 8bits(指数位) 23bits(尾数位)
    double:
    1bit(符号位) 11bits(指数位) 52bits(尾数位)

复习:

float a=5.8;

5.8=1.45*2^2;

0 129 0.45
0 10000000 01 1100 1100 1100 1100 1100 1

为了方便计算机比较大小,指数表示为127+exponent。比如2就是129

科学技术法表示128 可以12.8 * 10 、1.28 * 10^2 、 0.128 * 10^3 IEEE754标准规定浮点数使用1.xx,因此1是被省略的。

可以看出来float最大能表示的数取决于指数:2^2^8=3.4*10^38

整型提升

Java定义了若干使用于表达式的类型提升规则:
1)所有的byte型. short型和char型将被提升到int型
2)如果一个操作数是long形 计算结果就是long型;
3)如果一个操作数是float型,计算结果就是float型;
4)如果一个操作数是double型,计算结果就是double型;

+=,++ 没有这个问题
byte a=1;
byte b=1;
byte c=a+b; //error

ldc 指令

从运行时常量区取出int,double,String等类型,并将其压入操作数栈顶。

HashMap modCount

hashMap中使用modCount来维护多线程中同时遍历与修改同一个HashMap的快速失败机制。
在JDK 1.7 中modCount是由volatile修饰的,但是1.8不再由这个关键字修饰。因为开发者觉得没有必要,且在单线程环境下消耗的性能太高。

相关连接:Oracle Java Bug Database

延迟初始化

HashMap 在初始化的时候,并没有为Node数组申请内存,而是放在了第一次put的时候进行resize操作。在平时开发也可以将有性能消耗的操作进行lazy initial

等号操作符

等号操作符会返回赋值的结果,比如

System.out.println(a=3);

会返回3,这也是一种编程技巧,比如:

if((a=3)==4){

}

会在比较过程中进行对a进行赋值

关闭逻辑运算的短路效果

有时候我们想要比较两个操作是不是都为false

    public static boolean getTrue() {
        System.out.println("运行getTrue方法");
        return true;
    }

    public static boolean getFalse() {
        System.out.println("运行getFalse方法");
        return false;
    }

    public static void main(String[] args){
       if (getTrue() || getFalse()){
           System.out.println("判断成功");
       }
    }

输出:
运行getTrue方法
判断成功

我们都知道逻辑运算中会有短路操作,但是某些时候我们想要在判断的同时让两个方法都运行的话,一般会改成这样:

public static void main(String[] args){

       boolean isTure=getTrue();
       boolean isFalse=getFalse();
       if (isTure || isFalse){
           System.out.println("判断成功");
       }
    }

虽然达到了效果,但是代码远不如上面的简洁,这个时候我们可以使用位运算:

    public static void main(String[] args){
       if (getTrue() | getFalse()){
           System.out.println("判断成功");
       }
    }

一样能达到上面的效果

参见Oracle Java Doc 15.22.2

快捷表示2的幂

程序中想要快捷表示2的幂,可以直接用位运算

int a =1<<3; //8
int a =1<<4; //16

主要可用于表示大数字的时候,也不用担心性能,因为编译器会帮你转换为具体的值。

正则表达式

正则表达式在查找的时候不会复用以前的字符串,比如
\w\d\w
在匹配A1B2C3D的时候,会拿到
A1B
C3D
的结果,而B2C会被丢失。

resize()方法

JDK1.7 中

HashMap在resize()扩容在多线程情况下可能会生成一个死循环链表、
详细:
主要在于链表的重新排列。由于hash()与扩容带来的链表位置的不确定,HashMap为了保证性能,通过倒置排列链表的方法将原本复制一个链表需要O(n)的时间复杂度降低到了O(1)。

这样带来的问题就是多线程可能会生成一个死循环链表。

关键代码:

while(null != e) {
    Entry<K,V> next = e.next; //1
    if (rehash) {
        e.hash = null == e.key ? 0 : hash(e.key);
    }
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;//2
}

关键点:

  • 提前获取了next
  • 链表会倒置排列

比如a->b->c ,线程1提前记录a的下一个元素为b,也就是a->b,暂停执行线程2,线程2完全执行,链表变成了c->b->a,这个时候继续线程1,线程1执行b.next会发现b的next是元素a,就变成了a->b->a 就变成了无限循环。

详解:老生常谈,HashMap的死循环—占小狼

而JDK1.8 解决了这个问题,关键点在于:

resize()操作都是以2的幂数进行扩容的,这样扩容后可以发现节点要么在原位置,要么在原位置乘以2的位置。这个时候我们可以申请两个节点,一个记录头,一个记录尾。每次替换节点的时候都把节点插入尾部,最后把数组指向头,这样就不会有倒置链表的问题了。

伪代码:将node链表按奇偶分开:


Node<Integer> nHead = null, nTail = null; Node<Integer> oHead = null, oTail = null; Node<Integer> n = node; do { //偶数 if (n.item % 2 == 0) { if (oHead == null) { oHead = n; } else { oTail.next = n; } oTail = n; } //基数 else { if (nHead == null) { nHead = n; } else { nTail.next = node; } nTail = n; } } while ((n = node = node.next) != null);

HashMap只用维护两个这样的nHead节点即可。

正则表达式\w

\w 官方说明是匹配字母、数字、下划线、汉字。

但是具体是不是包括汉字,还是得依靠语言环境操作系统等。\w涉及到汉字的时候不一定可靠。

PriorityQueue 优先队列

Java 官方文档:PriorityQueue在方法提供的迭代是保证遍历优先级队列中的元素的任何特定顺序。如果您需要有序遍历,请考虑使用Arrays.sort(pq.toArray())这是因为当在迭代器是包含一个remove操作的,而迭代器在中执行remove操作时,可能会涉及到一个未访问的元素被移动到了一个已经访问过的节点位置.
相关连接:Java Docs PriorityQueue

HashMap扩容

hashMap扩容时间复杂度是O(n),n为hashMap已存的元素数量,一般为(length*0.75),并且使用默认的容量扩容刚开始是16,依次32,64,128因此如果会有大量的数据的话,尽量先指定容量大小,避免频繁扩容带来的性能消耗。并且Java 8 的hashMap性能比Java 7在各种情况下都高很多。

Static 初始化

静态变量初始化的时间为类加载过程的初始化阶段,类加载触发时间段为:

  • 使用new 关键字实例化对象
  • 调用某个类静态方法时
  • 读取或设置类的静态字段
  • 反射Class.ForName()
  • 初始化子类
  • 虚拟机表明启动类

的时候,会触发。

而成员变量初始化发生在对象实例化的过程中。实例化对象需要:加载连接初始化完成后才能使用。注意是完成.

但是在初始化静态变量的时候一般也会带着其他类的实例化

比如:

public class B{
    public static A a=new A();
}

这个时候在初始化类B的时候,会带着类A的对象的实例化

再看:

public class B{
    public static B a=new B();
}

这样写就会将B的实例化阶段提前到初始化阶段。

public class B{
    public static B a=new B();
    public static int a=1;

    public B(){
        System.out.println(a);
    }
}

public static void main(String[] args) {
    B b=new B();
} 
打印:
0
1

这便是在初始化B的时候,提前将B的对象的实例化提前到了初始化阶段,而类B的整个初始化阶段还没完成,这样就导致了第一次输出默认初始化的值:0.

因此在写单例类定义instance字段的时候,最好写在所有static变量后面,防止在构造函数里面使用其他static字段,导致空指针异常

类初始化

public class Test {

    private int a=1;

    private int b;

    public Testt(){

        b=2;
    }
}

javap -c Test.class

  public Collection.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_1
       6: putfield      #2                  // Field a:I
       9: aload_0
      10: iconst_2
      11: putfield      #3                  // Field b:I
      14: return
}

可以看到,虽然Java初始化成员变量可以在定义的时候初始化,也可以在构造函数里面初始化,但是其实经过编译后可以发现,定义时初始化只是一个语法糖,最后编译出来的代码还是会按顺序转移到构造函数里面统一初始化,也就是说所有的成员变量的初始化都是通过构造方法初始化的