Java基础是每个Android程序员必备的,因为语法糖和封装以及编译器优化的存在我们可以不用关心一些细节的实现,但是一旦发生bug或者代码的优化,Java的细节基础就很关键。本文来自日常开发和随时随地的coding灵感不时更新。
本文代码基于Win64位版本Java8,使用intellij idea开发工具,并添加环境变量。
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)
我们都知道Java代码运行的机制:
Java虚拟机,java源文件(.java)通过编译器生成字节码文件(.class),字节码文件(.class)通过JVM(Java虚拟机)中的解释器再翻译成特定机器上的机器码
一般来说我们只要编写正确的.java源代码就没有问题,但是因为编译优化的存在,我们编写的代码和最后运行的代码是不同的。
例如:IntegerTest文件保存地址:D:\idea_workspace\MyGroovyDemo\src\javaTest\IntegerTest
使用命令行编译:
cd D:\idea_workspace\MyGroovyDemo\src\javaTest
javac IntegerTest.java因为中文注释的存在会发生错误: 编码GBK的不可映射字符
javac -encoding “UTF-8” IntegerTest.java //指定为编码UTF-8
运行以上命令行,会在.java同级目录下生产.class文件,可以使用idea直接打开。
首先Integer包装类是对int基本类型的包装是毋庸置疑的。但是既然涉及到包装操作肯定就会比基本类型复杂一些。
有关常量池,我们需要明确对它目的:
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
先看以下代码,注释说明输出结果
/**
* Created by 李可乐 on 2017/5/15.
*/
public class IntegerTest {
public static void main(String[] args){
Integer v1=40;
Integer v2=40;
System.out.println(v1==v2);//输出 true
Integer v3=128;
Integer v4=128;
System.out.println(v3==v4);//输出 false
}
}
为什么基本相同的代码,会因为数值的大小产生不同的结果,这就是常量池的存在。
首先我们不说原理,先看上面这段代码编译后的class文件,看编译器做了什么。
运行的命令行编译源码,结果如下图,左边是.java源码,右边是.class字节码被idea反编译出来的结果。
这就是源码编译后我们的=
赋值操作其实调用了Integer.valueOf()
静态方法。
然后追踪源码:
静态方法
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
常量池:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
看到jdk的源码什么都明白了,一个IntegerCache
常量池类内部维护了一个Integer[]
数组,数组大小127+128+1=256,其中数值范围[-128, 127],因为数组下标和对应的数值有对应关系,所以Integer.valueOf()
在确定入参落在常量池的范围内,就直接通过计算下标访问数组得到结果,如果范围之外就直接new创建新的对象返回(直接返回这和String常量池机制不同)。另外从IntegerCache
常量池的源码上我们还可以知道通过参数:java.lang.Integer.IntegerCache.high
可以调整池的正整数上限。其实源码的英文注释也有详细说明。
Integer常量池机制:以数组形式存放,使用数值间的对应关系,数组下标和数组项具有对应关系
源自这篇博文Java常量池详解之一道比较蛋疼的面试题。
public static void objPoolTest() {
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println((i1 == i2));//常量池机制 会返回true
System.out.println(i1 == i2 + i3);//常量池的对象 结果是什么?
System.out.println(i4 == i5);//它们都直接new创建对象 指向不同的内存地址 结果为:false
System.out.println(i4 == i5 + i6);//直接创建的对象 结果又是什么?
}
针对上面的代码我们还是通过编译-反编译方式分析
通过反编译,可以看到Integer对象间的+
操作其实int基本类型间加操作。
通过反编译结果我们来到Integer源码,下面抽取关键代码。
/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;
/**
* Returns the value of this {@code Integer} as an
* {@code int}.
*/
public int intValue() {
return value;
}
结合源码,我们知道包装类为什么叫包装类,它内部持有真正的数据,使用一些语法糖支持对包装类的语句操作,方便我们使用包装类。
基本类型和包装类的区别:
区别 | == | equals() |
---|---|---|
基本类型 | 值 | 不可用 |
包装类 | 内存地址 | 内容 |
基于上表,在使用包装类的时候除非有特别目的一般都建议使用equals()
比较数据。
首先要明白String不是对任何基本类型的包装,String内部持有的其实是char[]
数组对象
/** The value is used for character storage. */
private final char value[];
看下面的代码:
public class StringTest {
public static void main(String[] args){
String value1="value";
String value2="value";
String value3=new String("value");
String value4=new String("value").intern();
System.out.println(value1==value2);//输出 true
System.out.println(value1==value3);//输出 false
System.out.println(value1==value4);//输出 true
}
}
value
值,放入该值,当value2构建相同的值时能够从常量池中找到,它们都指向相同的内存地址。new String("value")
很明显的构造了Srtring对象,没有使用常量池机制,比对时各自指向不同的内存地址。第三个true:就有点意思了。
/**
* Returns a canonical representation for the string object.
*
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
*
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
*
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
*
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* The Java™ Language Specification.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
因为它是native方法,所以没有java代码实现,我们可以看注释。从注释上我们可以得知,通过该方法即使是在使用new明确的构造对象的情况下,也能引入了常量池机制。当该方法被调用时,如果常量池中存在该值直接返回,不存在则构造新对象并且存入常量池之后再返回。
更多解读在:深入解析String#intern
String常量池机制:以Map< key,value >形式存放,以String对象的hashCode作为key索引存入map中
基于对常量池的理解,我们在StringTest加入以下代码:
String value5="value"+"value";
String value6="value"+"value";
String value7=value1+value1;
String value8="value"+value1;
String value9=("value"+value1).intern();
System.out.println(value5==value6);//输出 true
System.out.println(value5==value7);//输出 false
System.out.println(value5==value8);//输出 false
System.out.println(value5==value9);//输出 true
第一个true很关键,从代码上我们看到都是基于常量池中对象的拼接,但是怎么就返回true都指向相同内存地址呢。
我们编译-反编译看看编译器做了什么:
)
通过看图已经很能说明问题了,基于双引号两个字符串的拼接的String,在编译期就被优化成了一个字符串。所以value5和value6都被优化了,而value7和value8都不是单纯的基于双引号字符串拼接,编译器不知道怎么优化就没有修改代码。而value9就体现了intern对的常量池操作。
其实我挺喜欢语法糖(Syntactic Sugar)这个称呼的。
在java7之后switch语句可以支持String类型。这其实就是语法糖。
先看看基本int作为switch判断类型时候的反编译代码:
这里有个值得关注的点需要说明,上图中int key=0
使用的数值比较小,编译器优化使用byte表示,而一个byte能够表示的范围是[-128,127],如果我们修改源码,把key的值调大,相应的编译器会选择能够表示更大数据范围的基本类型类表示。
然后我们来看看String作为key时,编译器做了什么:
本质上还是和int作为key时一样的代码优化,先使用hashCode()
方法计算赋值给byte,然后就和int-switch一样了。
这里就需要强调,不同于基本类型作为key的编程习惯和考虑的维度(基本类型有默认值且非空),String作为key判断值时需要case覆盖条件还需要考虑null状态,如果String对象为空,在switch语句中调用hashCode()
就会NullPointerException空指针异常。而基本类型当case
不能覆盖条件时会进入default
首先要说的是Boolean包装类也是有常量池的,因为布尔值的特性(只有true和false)它的常量池机制就非常的简单:
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code true}.
*/
public static final Boolean TRUE = new Boolean(true);
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code false}.
*/
public static final Boolean FALSE = new Boolean(false);
&&
明显的逻辑运算,编译器能够处理成正确的代码说到布尔值的逻辑运算,就让我联想到二进制(0/1)和布尔值(false/true)的关系。
下面来说说一道很有意思的逻辑题:
有 1000 个一模一样的瓶子,其中有 999 瓶是普通的水,有一瓶是毒药。任何喝下毒药的生物都会在一星期之后死亡。现在,你只有 10 只小白鼠和一星期的时间,如何检验出哪个瓶子里有毒药?
先把问题的规模缩小,让它看起来简单一些。
8瓶水其中一瓶有毒药,3只老鼠,同时进行实验。
首先以程序员的角度思考问题,8和3的关系,其实就是二进制2^3=8的关系。看我灵魂画手上图:
画图就是最形象的关系描述。箭头上的每个圆角矩形就对应一只老鼠,它负责某个二进制中的位为1目标,互相之间负责的对象有重叠。当实验结果出来,负责哪位的老鼠死掉就说明该位为1,3个二进制位换算就能得到实验结果知道哪瓶是毒药。
这个好像是大学的学的《计算机原理》《数字电路》的内容吧。
同样的问题把规模扩大,1000瓶水有1瓶毒药,10个老鼠,同时实验。其实就是2^10=1024,只是题目没有这么明显数量关系。
为什么我脑回路这么长在boolean布尔值章节说二进制,大概就是回想起当年被《数字电路》的逻辑与或非
被支配的恐惧吧。