java笔记
=====第一个java程序=====
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
命令
$ javac HelloWorld.java
$ java HelloWorld
Hello World
javac 后面跟着的是java文件的文件名,例如 HelloWorld.java。
该命令用于将 java 源文件编译为 class 字节码文件
java 后面跟着的是java文件中的类名,例如 HelloWorld 就是类名,
如: java HelloWorld。
=====package=====
1.解决类名冲突的问题
2.同一个包中的类不需要被导入,当代码使用外部包中的类时,需要用import语句导入包含该类的包。
3.代码使用外部包中的类,另外一个方法是在代码中使用类的完全限定名称。例如,在使用Scanner的代码中,如果省略了导入Scanner的语句,则需要在使用Scanner类的位置使用Java.util.Scanner
4.Java编译器默认为所有的Java程序引入了JDK的Java.lang 包中的所有的类。其中定义了一些常用类:System、String、Object、Math等。因此我们可以直接使用这些类,而不必显式引入。但使用其他包中的类时,则必须先引入、后使用。
5.把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用
6.包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类
7.package 包名
8.包声明一定作为源代码的第一行
9.包的名称一般为小写
10.如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略。
11.通常,一个公司使用它互联网域名的颠倒形式来作为它的包名.例如:互联网域名是 runoob.com,所有的包名都以 com.runoob 开头。包名中的每一个部分对应一个子目录.例如:有一个 com.runoob.test 的包,这个包包含一个叫做 Runoob.java 的源文件,那么相应的,应该有如下面的一连串子目录:
....\com\runoob\test\Runoob.java
=====import=====
1.import Java.(包名).(方法名);
2.
它不像#include 一样,会将其他java文件的内容载入进来。import 只是让编译器编译这个java文件时把没有姓的类别加上姓,并不会把别的文件程序写进来。你开心的话可以不使用import,只要在用到类别的时候,用它的全部姓名来称呼它就行了(就像例子一开始那样),这样跟使用import功能完全一样。
3.
import java.lang.*;
import java.io.*;
表示文件里面说到的类不是java.lang包的就是java.io包的。编译器会帮我们选择与类名对应的包。
4.
单类型导入(single-type-import)
(例:import java.util.ArrayList; )
按需类型导入(type-import-on-demand)
(例:import java.util.*;)
java以这样两种方式导入包中的任何一个public的类和接口(只有public类和接口才能被导入)
上面说到导入声明仅导入声明目录下面的类而不导入子包,这也是为什么称它们为类型导入声明的原因。
5.
导入的类或接口的简名(simple name)具有编译单元作用域。这表示该类型简名可以在导入语句所在的编译单元的任何地方使用.这并不意味着你可以使用该类型所有成员的简名,而只能使用类型自身的简名。
例如: java.lang包中的public类都是自动导入的,包括Math和System类.但是,你不能使用它们的成员的简名PI()和gc(),而必须使用Math.PI()和System.gc().你不需要键入的是java.lang.Math.PI()和java.lang.System.gc()。
6.
程序员有时会导入当前包或java.lang包,这是不需要的,因为当前包的成员本身就在作用域内,而java.lang包是自动导入的。java编译器会忽略这些冗余导入声明(redundant import declarations)。即使像这样
import java.util.ArrayList;
import java.util.*;
多次导入,也可编译通过。编译器会将冗余导入声明忽略.
7.
在Java程序中,是不允许定义独立的函数和常量的。即什么属性或者方法的使用必须依附于什么东西,例如使用类或接口作为挂靠单位才行(在类里可以挂靠各种成员,而接口里则只能挂靠常量)。
如果想要直接在程序里面不写出其他类或接口的成员的挂靠单元,有一种变通的做法 :
将所有的常量都定义到一个接口里面,然后让需要这些常量的类实现这个接口(这样的接口有一个专门的名称,叫(“Constant Interface”)。这个方法可以工作。但是,因为这样一来,就可以从“一个类实现了哪个接口”推断出“这个类需要使用哪些常量”,有“会暴露实现细节”的问题。
8.
J2SE 1.5里引入了“Static Import”机制,借助这一机制,可以用略掉所在的类或接口名的方式,来使用静态成员。static import和import其中一个不一致的地方就是static import导入的是静态成员,而import导入的是类或接口类型。
如下是一个有静态变量和静态方法的类
package com.assignment.test;
public class staticFieldsClass {
static int staticNoPublicField = 0;
public static int staticField = 1;
public static void staticFunction(){}
}
平时我们使用这些静态成员是用类名.静态成员的形式使用,即staticFieldsClass.staticField或者staticFieldsClass.staticFunction()。
现在用static import的方式:
//**精准导入**
//直接导入具体的静态变量、常量、方法方法,注意导入方法直接写方法名不需要括号。
import static com.assignment.test.StaticFieldsClass.staticField;
import static com.assignment.test.StaticFieldsClass.staticFunction;
//或者使用如下形式:
//**按需导入**不必逐一指出静态成员名称的导入方式
//import static com.assignment.test.StaticFieldsClass.*;
public class StaticTest {
public static void main(String[] args) {
//这里直接写静态成员而不需要通过类名调用
System.out.println(staticField);
staticFunction();
}
}
这里有几个问题需要弄清楚:
Static Import无权改变无法使用本来就不能使用的静态成员的约束,上面例子的StaticTest和staticFieldsClass不是在同一个包下,所以StaticTest只能访问到staticFieldsClass中public的变量。使用了Static Import也同样如此。
导入的静态成员和本地的静态成员名字相同起了冲突,这种情况下的处理规则,是“本地优先。
不同的类(接口)可以包括名称相同的静态成员。例如在进行Static Import的时候,出现了“两个导入语句导入同名的静态成员”的情况。在这种时候,J2SE 1.5会这样来加以处理:
如果两个语句都是精确导入的形式,或者都是按需导入的形式,那么会造成编译错误。
如果一个语句采用精确导入的形式,一个采用按需导入的形式,那么采用精确导入的形式的一个有效。
大家都这么聪明上面的几个特性我就不写例子了。
static import这么叼那它有什么负面影响吗?
答案是肯定的,去掉静态成员前面的类型名,固然有助于在频繁调用时显得简洁,但是同时也失去了关于“这个东西在哪里定义”的提示信息,理解或维护代码就呵呵了。
但是如果导入的来源很著名(比如java.lang.Math),这个问题就不那么严重了。
9.
使用按需导入声明是否会降低Java代码的执行效率?
绝对不会!
一、import的按需导入
import java.util.*;
public class NeedImportTest {
public static void main(String[] args) {
ArrayList tList = new ArrayList();
}
}
编译之后的class文件 :
//import java.util.*被替换成import java.util.ArrayList
//即按需导入编译过程会替换成单类型导入。
import java.util.ArrayList;
public class NeedImportTest {
public static void main(String[] args) {
new ArrayList();
}
}
二、static import的按需导入
import static com.assignment.test.StaticFieldsClass.*;
public class StaticNeedImportTest {
public static void main(String[] args) {
System.out.println(staticField);
staticFunction();
}
}
上面StaticNeedImportTest 类编译之后 :
//可以看出 :
//1、static import的精准导入以及按需导入编译之后都会变成import的单类型导入
import com.assignment.test.StaticFieldsClass;
public class StaticNeedImportTest {
public static void main(String[] args) {
//2、编译之后“打回原形”,使用原来的方法调用静态成员
System.out.println(StaticFieldsClass.staticField);
StaticFieldsClass.staticFunction();
}
}
这是否意味着你总是可以使用按需导入声明?
是,也不是!
在类似Demo的非正式开发中使用按需导入声明显得很有用。
然而,有这四个理由让你可以放弃这种声明:
编译速度:在一个很大的项目中,它们会极大的影响编译速度.但在小型项目中使用在编译时间上可以忽略不计。
命名冲突:解决避免命名冲突问题的答案就是使用全名。而按需导入恰恰就是使用导入声明初衷的否定。
说明问题:毕竟高级语言的代码是给人看的,按需导入看不出使用到的具体类型。
无名包问题:如果在编译单元的顶部没有包声明,Java编译器首选会从无名包中搜索一个类型,然后才是按需类型声明。如果有命名冲突就会产生问题。
===== equals和==的区别 =====
1.值类型是存储在内存中的堆栈(简称栈),而引用类型的变量在栈中仅仅是存储引用类型变量的地址,而其本身则存储在堆中。
2.==操作比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量在堆中存储的地址是否相同,即栈中的内容是否相同
3.equals操作表示的两个变量是否是对同一个对象的引用,即堆中的内容是否相同。
4.==比较的是2个对象的地址,而equals比较的是2个对象的内容,显然,当equals为true时,==不一定为true
===== 数据类型 =====
1.boolean 类型数据只允许取值true 或 false(不可以使用0 或非0的整数来代替true和false,区分于C语言)。
2.基本数据类型:byte、short、int、long、float、double、char、boolean
引用数据类型:对象、数组等,另外为符合面向对象特征,Java中每一种基本数据类型都有对应的包装类:Byte、Short、Integer、Long、Float、Double、Character、Boolean,并且提供自动拆装箱功能。
3.
存储在什么地方?
1.寄存器(register):由于寄存器是在CPU内部的,所以它的速度最快,但是数量有限,所以由编译器根据需求进行分配。
2.栈(stack):位于通用RAM中,通过栈指针的移动来分配和释放内存,指针向下移动分配新的内存;指针向上移动则释放内存。速度仅次于寄存器。创建程序时,Java编译器必须知道存储在栈内所有数据的确切大小和生命周期,因为它必须生成相应的代码,以便上下移动栈指针,这就限制了程序的灵活性。所以java中的对象并不存放在栈当中,但对象的引用存放在栈中。
3.堆(heap):也是位于RAM中的内存池,用于存放所有的JAVA对象。编译器不需要知道要从堆里分配多少存储区域,也不需要知道存储的数据在堆里面存活多长时间,因此堆要比栈灵活很多。当你new创建一个对象时,编译器会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代码。用堆进行存储分配比用栈进行存储存储需要更多的时间。
4.静态存储(static storage):这里的“静态”是指“在固定的位置”(也在RAM里)。静态存储里存放程序运行时一直存在的数据。你可用关键字static来标识一个对象的特定元素是静态的,即存放类中的静态成员,但JAVA对象本身从来不会存放在静态存储空间里。
5. 常量存储(constant storage):存放字符串常量和基本类型常量(public static final)。常量值通常直接存放在程序代码内部,它们永远不会被改变。有时,在嵌入式系统中,常量本身会和其他部分分割离开,所以在这种情况下,可以选择将其放在ROM中。
简单描述下垃圾回收机制
垃圾回收回收的是无任何引用的对象占据的内存空间(堆)而不是对象本身,要注意以下3点:
1)对象可能不会被回收,即垃圾回收不一定会执行;
2)垃圾回收并不等于析构;+
3)垃圾回收只与内存有关。
引用计数器:一种简单但是速度很慢的垃圾回收策略。即每个对象都有一个引用计数器,当有引用连接至对象时计数器加1;当引用离开时计数器减1。垃圾回收器会在含有全部对象的列表中遍历,发现某个对象的引用计数器为0时,就释放其占用的内存。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。
自适应、分代的、停止——复制、标记——清扫 垃圾回收方法:
停止——复制:先暂停程序的运行,然后将所有活的对象从当前堆复制到另一个堆,没有被复制的都是垃圾。当对象从一个堆复制到另一个堆,它们的排列是一个挨着一个的,所以新堆保持紧凑排列。
标记——清扫:遍历所有的引用,找出所有活的对象,然后对它们进行标记,这个过程不会回收任何对象,只有全部标记工作完成时才开始清除工作。没有被标记的对象将会被释放,不发生任何复制动作,所以剩下的堆空间不是连续的。
创建了几个对象?
String s="abc"; 创建了几个对象?
毫无疑问,这里面只创建了一个对象——“abc";
String s1="abc"; String s2=s1;创建了几个对象?
仍然只有一个对象——“abc";
String s1="abc"; String s2=”abc";创建了几个对象?
这里仍然只有一个对象——“abc";
String s="abc"+"def";创建了几个对象?
注意,这里创建了三个对象:“abc"、”def"、“abcdef";
String s=new String("abc");创建了几个对象?
大家也都知道是两个对象。实际上是"abc"本身就是文字池中的一个对象,在运行new String()时,把文字池即pool中的字符串"abc"复制到堆中,并把这个对象的应用交给s,所以创建了两个String对象,一个在pool中,一个在堆中。
String s1=new String("abc");String s2=new String("abc");创建了几个对象?
三个对象。"abc"是文字池中的一个对象,然后又在堆中用new String()创建了两个对象。
===== 局部变量和成员变量 =====
1.
局部变量:方法体内有效。
成员变量:无论何种权限,都可以在类的所有方法中使用。
2.
局部变量:
不可自动初始化,要求在程序中显示地给其赋值,
只有当方法被调用执行时,局部变量才被分配内存空间,
调用完毕后,所占空间释放。
3.
成员变量:
可自动初始化,数值型为0,逻辑型为false,引用型
为null,可在声明是进行,也可在方法中实现,但不能在声明
和方法之间进行赋值
4.基本类型的局部变量如果值相同也都指向同一个地址。只有后面的和前面的不想等的时候才会指向新的地址,这就是为什么基本局部变量相互之间不会影响的原因。
5.局部变量和形参带final。
在一个线程A中开起另一个线程B,如果线程B要使用线程A的局部变量,那么A的局部变量需要定义成final。理由:局部变量是线程内部共享的,每一个线程内的不能访问其他线程的局部变量,但是上诉的情况却违背了这一原则,那么加上final为什么就可以了呢?原因是加上final之后,在创建B线程的时候会把final标记的变量作为线程B的构造方法的参数传给B,如此一来就解决了此问题,这是一个比较巧妙的做法,通过class文件反编译可以看出这个道理
6.Java的String变量比较特殊,他所定义的变量的值全部都存放在常量池里面,不管是不是final的。下面的结果全部为true。并且如果相等都指向同一个地址。
public class StringTest {
private static String s1 = "123";
static final String s2 = "123";
public static void main(String[] args) {
String s3 = "123";
final String s4 = "123";
System.out.println(s1 == s2);
System.out.println(s3 == s4);
System.out.println(s1 == s3);
}
}
一个我们编写的java源码类(机器码)要想被正式运行,必须先编译成字节码(class文件),然后虚拟机经过类加载过程后才能真正使用。
而这个类加载过程包括了对字节码 加载 验证 准备 解析 初始化等过程。在这个过程中,我们会对我们定义的成员变量进行两次初始化,一次赋默认初值(0值,boolean赋为false),一次赋我们定义的初值,如:
class Test{
int a = 2;
}
先赋0,再赋2.
而方法,需要进栈执行,这个过程是没有赋初值过程的。成员变量和局部变量赋不赋初值的原因就在这里,成员变量我们不主动初始化赋初值,有大佬照顾,给他赋零值,而局部变量,姥姥不疼,舅舅不爱,必须自力更生,我们必须主动初始化进行赋值,否则编译器不通过。
如果不再需要某个对象时,也就是不引用该对象,可以将引用类型变量赋值null,表示引用为空。 若创建的对象没有被任何变量所引用,JVM会自动回收它所占的空间。
===== 静态相关 =====
1.
public class test { //1.第一步,准备加载类
public static void main(String[] args) {
new test(); //4.第四步,new一个类,但在new之前要处理匿名代码块
}
static int num = 4; //2.第二步,静态变量和静态代码块的加载顺序由编写先后决定
{
num += 3;
System.out.println("b"); //5.第五步,按照顺序加载匿名代码块,代码块中有打印
}
int a = 5; //6.第六步,按照顺序加载变量
{ // 成员变量第三个
System.out.println("c");//7.第七步,按照顺序打印c
}
test() { // 类的构造函数,第四个加载
System.out.println("d");//8.第八步,最后加载构造函数,完成对象的建立
}
static { // 3.第三步,静态块,然后执行静态代码块,因为有输出,故打印a
System.out.println("a");
}
static void run() // 静态方法,调用的时候才加载// 注意看,e没有加载
{
System.out.println("e");
}
}
一般顺序:静态块(静态变量)——>成员变量——>构造方法——>静态方法
静态代码块(只加载一次) 2、构造方法(创建一个实例就加载一次)3、静态方法需要调用才会执行,所以最后结果没有e
2.
public class Print {
public Print(String s){
System.out.print(s + " ");
}
}
public class Parent{
public static Print obj1 = new Print("1");
public Print obj2 = new Print("2");
public static Print obj3 = new Print("3");
static{
new Print("4");
}
public static Print obj4 = new Print("5");
public Print obj5 = new Print("6");
public Parent(){
new Print("7");
}
}
public class Child extends Parent{
static{
new Print("a");
}
public static Print obj1 = new Print("b");
public Print obj2 = new Print("c");
public Child (){
new Print("d");
}
public static Print obj3 = new Print("e");
public Print obj4 = new Print("f");
public static void main(String [] args){
Parent obj1 = new Child ();
Parent obj2 = new Child ();
}
}
执行main方法,程序输出顺序为: 1 3 4 5 a b e 2 6 7 c f d 2 6 7 c f d
输出结果表明,程序的执行顺序为:
如果类还没有被加载:
1、先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关。
2、执行子类的静态代码块和静态变量初始化。
3、执行父类的实例变量初始化
4、执行父类的构造函数
5、执行子类的实例变量初始化
6、执行子类的构造函数
如果类已经被加载:
则静态代码块和静态变量就不用重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。
修饰变量:static 数据类型 变量名
修饰方法:【访问权限修饰符】 static 方法返回值 方法名(参数列表)
特点:
1.static可以修饰变量,方法
2.被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
3.在类被加载的时候,就会去加载被static修饰的部分。
4.被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
被static修饰的成员变量叫做静态变量,也叫做类变量,说明这个变量是属于这个类的,而不是属于是对象,没有被static修饰的成员变量叫做实例变量,说明这个变量是属于某个具体的对象的。
实例变量:每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。
静态变量:静态变量由于不属于任何实例对象,是属于类的,所以在内存中只会有一份,在类的加载过程中,JVM为静态变量分配一次内存空间。
被static修饰的方法也叫做静态方法,因为对于静态方法来说是不属于任何实例对象的,那么就是说在静态方法内部是不能使用this的,因为既然不属于任何对象,那么就更谈不上this了。
对象.静态变量(不推荐的)
如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量。
在静态方法中没有this关键字因为静态是随着类的加载而加载,而this是随着对象的创建而存在的。静态比对象优先存在。
静态可以访问静态的,但是静态不能访问非静态的。
非静态的可以去访问静态的。
===== 修饰符 =====
public,公共修饰符,被其修饰的类、属性或方法在项目中任意类中访问。
protected,保护修饰符,被其修饰的类、属性或方法在当前类所属包或当前类的子类中可访问。
default,默认修饰符,没有明确声明修饰符时默认采用此修饰符,被其修饰的类、属性或方法只能被当前类所属包中的类访问。
private,私有修饰符,被其修饰的类、属性或方法仅在当前类中可访问。
特别注意的内容:
default 修饰的类、属性或方法如果是在不同包下,即使是子类也无法访问。
protected 修饰的类、属性或方法可以在不同包子类中访问,但是无法通过该子类的实例进行访问。例如 A 是 B 的父类,两者分属不同包下,A 中的方法 a() 使用 protected 进行修饰,此时我们可以在 B 的方法 b() 中调用 super.a(),但是无法通过实例化进行调用, new B().a() 则无法调用。
===== 作用域 =====
在同一作用域范围的包裹下成员变量名和局部变量名是可以变量名相同的
在同一个作用域范围的包裹下局部变量和局部变量不可以变量名相同(作用域内不能重复命名)
在方法中使用变量的时候如果不指明使用成员变量还是局部变量,那么默认的就是使用局部的那个变量,但是如果局部变量超出了它本身的作用域范围则会失效,被JVM垃圾回收,那么则可以重复命名此变量,并使用最新定义的这个局部变量。
对象的作用域
Java对象不具备与主类型一样的存在时间。用new关键字创建一个Java对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码:
{
String s = new String("a string");
} /* 作用域的终点 */
那么句柄s,也就是引用会在作用域的终点处消失。然而,s指向的String对象依然占据着内存空间。在上面这段代码里,我们没有办法继续使用这个对象,因为指向它的唯一一个句柄已经超出了作用域的边界。
这样造成的结果是:对于用new创建的对象,只要我们愿意,它们就会一直保留下去。这个编程问题在C和C++里特别突出。在C++里遇到的麻烦最大:由于不能从语言获得任何帮助,所以在需要对象的时候,根本无法确定它们是否可用。而且最麻烦的是,在C++里,一旦完成工作,必须保证将对象手动清除。
这样便带来了一个有趣的问题。假如 Java 让对象依然故我,怎样才能防止它们大量充斥内存,并最终造成程序的“凝固”呢。在 C++里,这个问题最令程序员头痛。但 Java 以后,情况却发生了改观。 Java 有一个特别的“垃圾收集器”,它会查找用 new 创建的所有对象,并辨别其中哪些不再被引用。随后,它会自动释放由那些闲置对象占据的内存,以便能由新对象使用。这意味着我们根本不必操心内存的回收问题。只需简单地创建对象,一旦不再需要它们,它们就会自动离去。这样做可防止在 C++里很常见的一个编程问题:由于程序员忘记释放内存造成的“内存溢出”
===== 常量 =====
Java中的常量池实际上分为两种形态:
静态常量池:即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
运行时常量池:则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存 中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
定义
常量代表程序运行过程中不能改变的值。
语法格式
[访问修饰符] final 数据类型 常量名称 = 值;关键字final不可缺,常量名称要求必须大写。其中中括号内容是可选项,
特点
1.有关键字final
2.在Java编码规范中,要求常量名必须大写
3.必须声明,后使用。可以在声明时赋值,也可以在使用前任何时间赋值,但只能赋值一次。
注意:全局常量可以不手动赋值,系统会初始化这些全局常量的值。局部常量必须赋值,否则使用时编译错误
作用
1.代表常数(也称常用的值),在项目开发实践中,会把这些常用到的值抽取出来放到一个类中方便其他类中调用这些常量,这样既可以防止疏忽出错,还便于以后维护代码,也就是说只要修改个地方就可以了。
2.增强程序的可读性。使用一些有意义的名称代替一些值。例如 DOWN一看就知道这个常数是代表向下的意思。
例子
final double PI = 3.14;
public final double PI = 3.14;
在Java语法中,常量也可以首先声明,然后再进行赋值,但是只能赋值一次,
final int UP;UP = 1;
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
s1 == s2这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。
s1 == s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello";,所以s1 == s3成立。
s1 == s4当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,鬼知道s4被分配到哪去了,所以地址肯定不同
s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,所以不做优化,等到运行时,s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。
s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。
s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。
至此,我们可以得出三个非常重要的结论:
必须要关注编译期的行为,才能更好的理解常量池。
运行时常量池中的常量,基本来源于各个class文件中的常量池。
程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。
以上所讲仅涉及字符串常量池,实际上还有整型常量池、浮点型常量池等等,但都大同小异,只不过数值类型的常量池不可以手动添加常量,程序启动时常量池中的常量就已经确定了,比如整型常量池中的常量范围:-128~127,只有这个范围的数字可以用到常量池。
首先说明一点,在java 中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。
https://tech.teamtime.cc/tech/2020-01-30-openvpn%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/
以上是 java笔记 的全部内容, 来源链接: utcz.com/z/393457.html