# 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 PoolString Constant PoolString 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 Integer100);

Integer j=new Integer100);

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=0forint 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=80String s=String.valueOf(i<100?90100);

String s1=String.valueOf(i<100?90100.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){

ListString>list1=new ArrayListString>();

list1.add("A");

list1.add("B");

ListString>list2=new ArrayListString>();

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=10ListString>tagClouds=new ArrayListString>(tagCloudNum);

//初始化标签云,一般是从数据库读入,省略

Random rand=new Random();

forint 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=10ListString>tagClouds=new ArrayListString>(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。