# Java基础知识点
JAVA 2019-03-29# 概述
开发中会经常遇到的一些基础知识点,其运行的结果体现了掌握Java基础是多么重要。
# 1. 字符串的位置
代码
public static void main(String[]args){
String str1=1+2+"apples";
String str2="apples:"+1+2;
}
运行结果
str1="3 apples"
stt2="apples:12"
分析
这都源于Java
对加号的处理机制:在使用加号进行计算的表达式中,只要遇到String字符串
,则所有的数据都会转换为String
类型进行拼接,如果是原始数据,则直接拼接,如果是对象,则调用toString
方法的返回值然后拼接。在“+”表达式中,String
字符串具有最高优先级。
# 2. 使用String直接量赋值
代码
public class Client{
public static void main(String[]args){
String str1="中国";
String str2="中国";
String str3=new String("中国");
String str4=str3.intern();
//两个直接量是否相等
boolean b1=(str1==str2);
//直接量和对象是否相等
boolean b2=(str1==str3);
//经过intern处理后的对象与直接量是否相等
boolean b3=(str1==str4);
}
}
运行结果
true
false
true
分析
Java
为了避免在一个系统中大量产生String
对象(为什么会大量产生?因为String字符串是程序中最经常使用的类型),于是就设计了一个字符串池(也有叫做字符串常量池,String Pool
或String Constant Pool
或String Literal Pool
),在字符串池中所容纳的都是String字符串对象,它的创建机制是这样的:创建一个字符串时,首先检查池中是否有字面值相等的字符串,如果有,则不再创建,直接返回池中该对象的引用,若没有则创建之,然后放到池中,并返回新建对象的引用,这个池和我们平常所说的池概念非常相似。对于此例子来说,就是在创建第一个“中国”字符串时,先检查字符串池中有没有该对象,发现没有,于是就创建了“中国”这个字符串并放到池中,待再创建str2字符串时,由于池中已经有了该字符串,于是就直接返回了该对象的引用,此时,str1和str2指向的是同一个地址,所以使用“==”来判断那当然是相等的了。- 那为什么使用new String(“中国”)就不相等了呢?因为直接声明一个String对象是不检查字符串池的,也不会把对象放到池中,那当然“==”为false了。
- 那为什么使用
intern
方法处理后就又相等了呢?因为intern
会检查当前的对象在对象池中是否有字面值相同的引用对象,如果有则返回池中对象,如果没有则放置到对象池中,并返回当前对象。
# 3. 谨慎包装类型的大小比较
代码
public class Client{
public static void main(String[]args){
Integer i=new Integer(100);
Integer j=new Integer(100);
compare(i, j);
}
//比较两个包装对象大小
public static void compare(Integer i, Integer j){
System.out.println(i==j);
System.out.println(i>j);
System.out.println(i<j);
}
}
}
运行结果
false
false
false
分析
i==j 在Java中“==”是用来判断两个操作数是否有相等关系的,如果是基本类型则判断值是否相等,如果是对象则判断是否是一个对象的两个引用,也就是地址是否相等,这里很明显是两个对象,两个地址,不可能相等。 i>j和i<j 在Java中,“>”和“<”用来判断两个数字类型的大小关系,注意只能是数字型的判断,对于Integer包装类型,是根据其intValue()方法的返回值(也就是其相应的基本类型)进行比较的(其他包装类型是根据相应的value值来比较的,如doubleValue、floatValue等),那很显然,两者不可能有大小关系的。
# 4. 自增的陷阱
代码
public class Client{
public static void main(String[]args){
int count=0;
for(int i=0;i<10;i++){
count=count++;
}
System.out.println("count="+count);
}
}
}
运行结果
count=0
分析
count++是一个表达式,是有返回值的,它的返回值就是count自加前的值,Java对自加是这样处理的:首先把count的值(注意是值,不是引用)拷贝到一个临时变量区,然后对count变量加1,最后返回临时变量区的值。程序第一次循环时的详细处理步骤如下:- 步骤1 JVM把count值(其值是0)拷贝到临时变量区。
- 步骤2 count值加1,这时候count的值是1。
- 步骤3 返回临时变量区的值,注意这个值是0,没修改过。
- 步骤4 返回值赋值给count,此时count值被重置成0。
# 5. 三元操作符的类型务必一致
代码
public class Client{
public static void main(String[]args){
int i=80;
String s=String.valueOf(i<100?90:100);
String s1=String.valueOf(i<100?90:100.0);
System.out.println("两者是否相等:"+s.equals(s1));
}
}
运行结果
两者是否相等:false
分析
三元操作符类型的转换规则:- 若两个操作数不可转换,则不做转换,返回值为Object类型。
- 若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,int类型转换为long类型,long类型转换为float类型等。
- 若两个操作数中有一个是数字S,另外一个是表达式,且其类型标示为T,那么,若数字S在T的范围内,则转换为T类型;若S超出了T类型的范围,则T转换为S类型(可以参考“建议22”,会对该问题进行展开描述)。
若两个操作数都是直接量数字(Literal),则返回值类型为范围较大者。
在变量s中,三元操作符中的第一个操作数(90)和第二个操作数(100)都是int类型,类型相同,返回的结果也就是int类型的90,而变量s1的情况就有点不同了,第一个操作数是90(int类型),第二个操作数却是100.0,而这是个浮点数,也就是说两个操作数的类型不一致,可三元操作符必须要返回一个数据,而且类型要确定,不可能条件为真时返回int类型,条件为假时返回float类型,编译器是不允许如此的,所以它就会进行类型转换了,int型转换为浮点数90.0,也就是说三元操作符的返回值是浮点数90.0,那这当然与整型的90不相等了。
# 6. 集合运算
代码
public static void main(String[]args){
List<String>list1=new ArrayList<String>();
list1.add("A");
list1.add("B");
List<String>list2=new ArrayList<String>();
list2.add("C");
list2.add("B");
//并集
list1.addAll(list2);
//交集
list1.retainAll(list2);
//差集
list1.removeAll(list2);
//无重复的并集
//删除在list1中出现的元素
list2.removeAll(list1);
//把剩余的list2元素加到list1中
list1.addAll(list2);
}
}
分析
为什么要介绍并集、交集、差集呢?那是因为只要去检查一下代码,就会发现,很少有程序员使用JDK提供的方法来实现这些集合操作,基本上都是采用的标准的嵌套for循环:要并集就是加法,要交集了就使用contains判断是否存在,要差集了就使用!contains(不包含),有时候还要为这类操作提供一个单独方法,看似很规范,但已经脱离了优雅的味道。
# 7. 使用shuffle打乱列表
常规代码
public static void main(String[]args){
int tagCloudNum=10;
List<String>tagClouds=new ArrayList<String>(tagCloudNum);
//初始化标签云,一般是从数据库读入,省略
Random rand=new Random();
for(int i=0;i<tagCloudNum;i++){
//取得随机位置
int randomPosition=rand.nextInt(tagCloudNum);
//当前元素与随机元素交换
String temp=tagClouds.get(i);
tagClouds.set(i, tagClouds.get(randomPosition));
tagClouds.set(randomPosition, temp);
}
}
使用shuffle
public static void main(String[]args){
int tagCloudNum=10;
List<String>tagClouds=new ArrayList<String>(tagCloudNum);
//打乱顺序
Collections.shuffe(tagClouds);
分析
使用一行代码,即可打乱一个列表的顺序,不用我们费尽心思的遍历、替换元素了。我们一般很少用到shuffle这个方法,那它可以用在什么地方呢?- 可以用在程序的“伪装”上。 比如我们例子中的标签云,或者是游戏中的打怪、修行、群殴时宝物的分配策略。
- 可以用在抽奖程序中。 比如年会的抽奖程序,先使用shuffle把员工排序打乱,每个员工的中奖几率就是相等的了,然后就可以抽取第一名、第二名。
- 可以用在安全传输方面。 比如发送端发送一组数据,先随机打乱顺序,然后加密发送,接收端解密,然后自行排序,即可实现即使是相同的数据源,也会产生不同密文的效果,加强了数据的安全性。
# 8. 实例变量和类变量
- 实例变量不允许采用
非法向前引用
public class Test
{
//下部代码将提示:非法向前引用
int var1 = var2 + 2;
int var2 = 20;
}
上面程序中定义 numl 成员变量的初始值时,需要根据 num2 变量的值进行计算,这就是非法前向引用
。因此, 编译上面程序将提示非法前向引用
的错误。
- 类似地, 两个类变量也不允许采用这种
非法向前引用
public class Test
{
//下部代码将提示:非法向前引用
static int var1 = var2 + 2;
static int var2 = 20;
}
- 但是,一个实例变量和一个类变量,则实例变量总是可以引用类变量,可以正常编译。
public class Test
{
//下部代码将提示:非法向前引用
int var1 = var2 + 2;
static int var2 = 20;
}
分析
上面程序中varl 是一个是实例变量,而var2是一个类变量。虽然 var2 位于varl之后被定义,但varl的初始值却可根据var2 计算得到。这是因为,var2变量是一个类变量,varl是实例变量,而类变量的初始化时机总是处于实例变量的初始化时机之前。所以,虽然源代码中 先定义了varl,再定义了var2,但var2 的初始化时机总是位于varl之前,因此varl变量的初始化可根据var2的值计算得到。
由于同一个JVM内每个类只对应一个 Class对象,因此同一个 JVM内的一个类的类变量只需要一块内存空间;但对于实例变量而言,该类每创建一次实例,就需要为实例变量分配一块内存空间。也就是说,程序中有几个实例,实例变量就需要几块内存空间。
# 9. 泛型
代码
List<Integer> listl = new ArrayList<>();
List<String > list2 = new ArrayList<> ();
System.out.println (listl .equals (list2));
输出结果
true
分析
因为Java
的泛型语法只用在编译时期
检查,执行时期
的类型信息自动抹除
,也就是执行时期实际上只会知道是Object类型
(又称为类型抹除)。由于无法在执行时期获得类型信息,编译程序只能就编译时期看到的类型来做检查,因此输出结果是true。