java小记

Java系统参数

系统级全局变量,该参数可以在程序中任何位置都可以访问到。优先级最高,覆盖程序中同名配置。
一般通过设置虚拟机参数来实现。
例如在idea中可以通过对应用程序进行配置:

Edit Configurations -> Add VM Options

java小记_第1张图片

系统参数的标准格式为:-Dargname=agrvalue

多个参数之间用空格隔开,如果参数值中间有空格,则用引号括起来。
虚拟机系统参数中设置的参数键值对,在程序中可以用 System.getProperty("propertyName") 获取对应参数值。

-X/-XX 为非标准系统参数形式,一般与 JVM 虚拟机设置有关,参数名和值都由 JVM 规范规定。例如:-Xms :初始堆大小、-Xmx :最大堆大小。

java小记_第2张图片

Java运行参数

main 方法执行时传入的参数值,如果参数有多个,用空格分开。
main 方法的一般格式为:public static void main(String[] args),其中,String[] args 就是存储运行参数的变量,在程序中可以直接使用。
Java运行参数仍然可以通过在Edit Configurations中设置
java小记_第3张图片

类似用法:
java小记_第4张图片

命令行中设置系统参数或运行参数

当然也可以在命令行中对这两种参数进行设置
java 命令的基本格式为 java [-options] class [args...],其中:

  • [-options] 配置 Java 系统参数
  • [args…] 配置 Java 运行参数

示例:java -Dfile.encoding=UTF-8 -Dmy=user Hi this is leo channel




字符串常量池(属于运行时常量池,只存储字符串)

参考博客(https://juejin.cn/post/693823...)(https://juejin.cn/post/685457...)

在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:

  • -XX:StringTableSize=99991

1.字符串常量池在Java内存区域的哪个位置

  • JDK7之前版本字符串常量池是放在方法区中的,不过自从JDK7之后,Hotspot虚拟机便将原本放在永久代的字符串常量池移至堆中。

2.字符串常量池是什么

  • 在HotSpot里实现的string pool功能的是一个固定大小的HashTable,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上,StringTable里面存的是key(字面量“abc”, 即驻留字符串)-value(字符串"abc"实例对象在堆中的引用)键值对。
  • 在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
  • 在JDK7.0中,StringTable的长度可以通过参数指定:
  • -XX:StringTableSize=66666

3.字符串常量池里放的是什么

  • 在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
  • 在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用地址。
  • 需要说明的是:字符串常量池中的字符串只存在一份!
    String s1 = "hello,world!";
    String s2 = "hello,world!";
    //即执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。

4.String a= "ha" 与 String a = new String("ha")区别

  • 直接定义的"ha"是储存在字符串常量池中;new String(“ha”)是String存储在堆中,“ha”在字符串常量池中;
  • 常量池中相同的字符串只会有一个,但是new String(“ha”)每new一个对象就会在堆中新建一个对象,不管这个值是否相同;
  • String a = “ha”与String b = “ha”:a b都指向字符串常量池中的“ha”,所以 a==b ;
  • String a = new String(“ha”)与String b = new String(“ha”):是会在堆中创建两个对象,这两个对象的值都为ha,所以a!=b;a.equals(b)返回true;
  • String a =“ha”在编译阶段就会在内存中创建,String a = new String(“ha”)是在运行时才会在堆中创建对象。

5.字符串拼接情况

    使用 ” ” 双引号创建 : String s1 = “first”;
    使用字符串连接符拼接 : String s2=”se”+”cond”;
    使用字符串加引用拼接 : String s12=”first”+s2;
    使用new String(“”)创建 : String s3 = new String(“three”);
    使用new String(“”)拼接 : String s4 = new String(“fo”)+”ur”;
    使用new String(“”)拼接 : String s5 = new String(“fo”)+new String(“ur”);
    s1 : 中的”first” 是字符串常量,在编译期就被确定了,先检查字符串常量池中是否含有”first”字符串,若没有则添加”first”到字符串常量池中,并且直接指向它。所以s1直接指向字符串常量池的”first”对象。
    s2:“se”和”cond”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,并且s2是常量池中”second”的一个引用。
    s12 : JVM对于字符串引用,由于在字符串的”+”连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即("first"+s2)无法被编译器优化,只有在程序运行期来动态分配使用StringBuilder连接后的新String对象赋给s12。
    (编译器创建一个StringBuilder对象,并调用append()方法,最后调用toString()创建新String对象,以包含修改后的字符串内容)
    s3 : 用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。但是”three”字符串常量在编译期也会被加入到字符串常量池(如果不存在的话)
    s4 : 同样不能在编译期确定,但是”fo”和”ur”这两个字符串常量也会添加到字符串常量池中,并且在堆中创建String对象。(字符串常量池并不会存放”four”这个字符串,会存放“fo”这个字符串)

6.final引用拼接

    public class StringConcat {
        final String a = "hello";
        final String b = "moto";
        String result = a + b + "2018";
    }
    • 对于final修饰的局部变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
    • 所以此时的(a + b + “2018”)和(“hello” + “moto” + “2018”)效果是一样的。



课外话关于intern()函数的额外知识点:
java小记_第5张图片
引用
关于为什么添加了一个append()函数之后结果不同的问题,参考博客(https://blog.csdn.net/qq_4491...)的解答

7.intern()方法

参考博客(https://tech.meituan.com/2014...
重点!!!(https://blog.csdn.net/qq_4491...

  • String.intern( )是一个Native方法,它的作用是:如果字符串常量池已经包含一个等于此String对象的字符串,则返回代表字符串常量池中这个字符串的String对象。【并不是堆中的字符串对象,而是字符串常量池中的字符串对象】;否则将此String对象的引用地址(堆中)添加到字符串常量池中,因为字符串常量池在堆中,所以没必要在字符串常量池中再多创建一份String对象。
  • jdk 1.7 后的字符串常量池存在于堆中。
  • jdk6中的常量池是放在 Perm(永久代) 区中的,Perm 区和正常的 JAVA Heap 区域是完全分开的。如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。
  • 再说说 jdk7 中的情况。这里要明确一点的是,在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。
  • 在intern()源码的注解上有说过一段话:
    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.翻译如下
    所有的文字字符串 ( 这里指的应该是用 双引号 "" 包裹起来的字符串,比如new String("a") 中的 “a”), 以及字符形常量 (个人理解, 这里指的是 String a = "abc" 中的 a. ) 都会具备 intern() 调用后的效果. 也就是, 前述两种字符串实例会进行判断、加入到字符串常量池中,也可以理解为前述两种定义自动调用 intern() 方法
    从这一段的注释我们可以推断出:
    ""包裹的字符串都会进入字符串常量池. 也就是说, String a = new String("string") 这一行代码, 会让 "string"本身作为一个字符串对象进入字符串常量池、而不是 a 实例进入字符串常量池.
    此外, 从 intern() 方法的注释我们可以知道, 在执行 intern() 方法时会发生以下事情:
    检验目标字符串对象的具体值在字符串常量池中是否存在, 不存在则则将目标对象的引用放入字符串常量池, 并返回目标字符串的引用; 否则返回字符串常量池里与目标字符串对象内容相同的对象引用
  • 举个例子说明,例如String s = new String("1") + new String("1"),这时候产生了两个对象,一个是s指向的对象在堆上(该对象为“11”),此时在常量池中还生成了”1“对象,所以为两个对象。在s调用intern()函数后,会把s引用的对象”11“地址返回给字符串常量池(jdk7以后常量池既可以存放对象也可以存放引用地址),这时候新建的一个变量s2,使得s2 = “11”,s2检测到常量池中有"11"的引用地址,就将存放在常量池中s的引用地址返回给s2,所以s == s2,由于等号比较的是引用是否相等(地址)(by the way,equals是调用对象的equals方法进行判断。在String类中,equals判断的是String内部用于存储字符串信息的char数组的每个元素是否完全相等),所以s等于s1,即两者地址相同,而这种情况,在jdk6之前则大相径庭,使用intern()函数之后,jdk6中是在字符串常量池中生成“11”这个字面量,而不是去存储放在堆中的”11“变量。
  • 使用intern()比不使用intern()消耗的内存更少
  • https://blog.csdn.net/vivianX... 该链接解释了为什么使用intern()函数会节省内存

    String aa=new String("abcdef");//"abcdef"字符串对象,创建在字符串常量池
    String aaIntern=aa.intern();//aaIntern为字符串常量池的"abcdef"对象
    System.out.println("aa==aaIntern    "+(aa==aaIntern));//false   
    String aaStr="abcdef";
    System.out.println("aaIntern==aaStr "+(aaIntern==aaStr));//true
    System.out.println("aa==aaStr   "+(aa==aaStr));//false  aa在堆,aaStr在字符串常量池           ——————————————  ——————————————  ——————————————  ——————————
    String bb = new String("123") + "456";
    String bbIntern = bb.intern();
    System.out.println("bb==bbIntern    " + (bb == bbIntern));// true,字符串常量池没有"123456"
    String bbStr = "123456";
    System.out.println("bb==bbStr   " + (bb == bbStr));// true
    System.out.println("bbIntern==bbStr " + (bbIntern == bbStr));// true
    String cc = new String("1") + "23";
    String ccIntern = cc.intern();// 字符串常量池已经有"123"
    System.out.println("cc==ccIntern    " + (cc == ccIntern));// false



关于自动装箱和自动拆箱

先说整型常量池

java中为了提高程序的执行效率,将[-128, 127]之间256个整数所有的包装对象提前创建好了,类加载时就已经创好了,放在了一个方法区的“整数常量池”当中。

目的是:如果一个整数范围在[-128, 127]里面的整数进行包装,包装时不需要再new对象了,直接从“整数常量池”中取出来。

池:就是缓存区的意思。缓存区的好处是:程序用起来执行很快,很方便。缺点是:如果没用到,就有点耗费了内存。

举例说,像Integer包装类,Integer类源码中有定义IntegerCache子类,该类用来在编译期间向常量池中添加Integer常量????
如果两个引用指向同一个对象,用==表示它们是相等的。如果两个引用指向不同的对象,用==表示它们是不相等的,即使它们的内容相同。即==判断的是两个对象的地址是否相同,===判断的是两个对象内的值是否相等。

Integer bInteger=127;

它实际上在内部的操作是:

Integer bInteger=Integer.valueOf(127);

valueOf(int i)函数的源码:

public static Integer valueOf(int i) {
        assert IntegerCache.high >= 127;
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

可以看到,如果值的范围在-128到127之间,它就从高速缓存返回实例。
所以…下面这两个指向同一个对象:

    Integer aInteger=127;
    Integer bInteger=127;

我们可以得到true。
另外,一个容易记错的点是

Integer i1 = new Integer(1)的时候是在Java堆中创建一个Integer对象,i1指向堆中的对象,i1与常量池没关系,而Integer i2 = 1, 所以i1==i2为false。
Integer e1 = 127;
Integer e2 = new Integer(127);
System.out.println(e1 == e2);  // false

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer[] cache;
        static Integer[] archivedCache;

        static {
            // high value may be configured by property  常量池中的最大值
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    h = Math.max(parseInt(integerCacheHighPropValue), 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            // Load IntegerCache.archivedCache from archive, if possible
            CDS.initializeFromArchive(IntegerCache.class);
            int size = (high - low) + 1;

            // Use the archived cache if it exists and is large enough
            if (archivedCache == null || size > archivedCache.length) {
                Integer[] c = new Integer[size];
                int j = low;
                for(int i = 0; i < c.length; i++) {
                    c[i] = new Integer(j++);
                }
                archivedCache = c;
            }
            cache = archivedCache;
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

在Java7之后,可以通过设置系统参数来扩大Integer常量池的最大值,即更改 -Djava.lang.Integer.IntegerCache.high=来修改

这是没主动去修改java.lang.Integer.IntegerCache.high的结果:

java小记_第6张图片

这是主动修改java.lang.Integer.IntegerCache.high的结果:

java小记_第7张图片




关于String的不可变性

参考至掘金博客:https://juejin.cn/post/6854573212886368270(关于String类的些许看法以及对StringTable(字符串常量池)的性能调优)

 String s = "hello";

这时候,"hello"字面量就被存放在StringTable(即字符串常量池)中,而变量s是一个引用,s指向了StringTable中的"hello"

java小记_第8张图片

当把s的值改为"hello world"

String s = "hello";
s = "hello world";

java小记_第9张图片

如何去验证是指向了一个新的字符串而不是修改其内容呢,我们可以打印一下hash值看看。

String s = "hello";
System.out.println(System.identityHashCode(s));
s = "hello world";
System.out.println(s.hashCode());
s = "hello";
System.out.println(System.identityHashCode(s));

java小记_第10张图片

可以看到,第一次和第三次的hash值一样,第二次hash值和其它两次不同,说明确实是指向了一个新的对象而不是修改了String的值。

那么String是怎么实现不可变的呢?我们来看一下String类的源码:
java小记_第11张图片

从源码中我们可以看出,首先String类是final的,说明其不可被继承,就不会被子类改变其不可变的特性;其次,String的底层其实是一个被final修饰的数组,说明这个value在确定值后就不能指向一个新的数组。这里我们要明确一点,被final修饰的数组虽然不能指向一个新的数组,但却是可以修改数组的值的:

java小记_第12张图片

既然可以被修改,那String怎么是不可变的呢?因为String类并没有提供任何一个方法去修改数组的值,所以String的不可变性是由于其底层的实现,而不是一个final。
那么String为什么要设计成不可变的呢?我觉得是因为出于安全性的考量,试想一下,在一个程序中,有多个地方同时引用了一个相同的String对象,但是你可能只是想在一个地方修改String的内容,要是String是可变的,导致了所有的String的内容都改变了,万一这是在一个重要场景下,比如传输密码什么的,不就出大问题了吗。所以String就被设计成了不可变的。

字符串的拼接

看下面一段程序

public static void main(String[] args) {
    String a = "hello";
    String b = " world!";
    String c = a+b;
}

我们来看一下这段代码对应的字节码指令:

java小记_第13张图片

重点看一下用红色标注的几行代码,看不懂前面的字节码指令没关系,可以看后面的注释。可以看到,字符串拼接其实就是调用StringBuilder的append()方法,然后调用了toString()方法返回一个新的字符串。




关于类加载时静态代码块、静态方法、静态变量、普通代码块、构造方法的执行顺序

首先定义父类:

public class Father {
    static int age = 10;

    static {
        System.out.println("Father静态代码块");
    }

    {
        System.out.println("Father普通代码块");
    }

    public static void leo(){
        System.out.println("Father静态方法");
    }

    public Father(){
        System.out.println("Father无参构造函数");
    }
}

子类继承父类:

public class Son extends Father{
    static {
        System.out.println("Son静态代码块");
    }

    {
        System.out.println("Son普通代码块");
    }

    public Son(){
        System.out.println("Son无参构造函数");
    }

    public static void main(String[] args) {
        Father.leo();
        new Son();
        System.out.println(Father.age);
    }
}

运行结果如下:

java小记_第14张图片

可以知道,首先当类加载的时候,会自动加载静态代码块静态代码块静态方法都是和类一起加载的,并且静态变量静态方法是根据调用顺序来执行的。

当我们运行Son类的时候,会发生什么呢?
首先当虚拟机加载Son类时,它会自动加载静态代码块,因为Demo继承了Father类,它会先去找Father类当中有没有静态代码块,如果有,则按顺序运行父类中的静态代码块。如果执行完或没有,则运行当前类的静态方法。故会依次输出 Father静态代码块–>Son静态静态代码块->Father静态方法。然后执行new Son(); 在执行子类的实例化之前,虚拟机又会去父类找普通代码块,如果有则执行,没有则会去调用父类的无参构造方法。随后去调用子类的普通代码块,然后才去执行子类的构造方法。所以会依次输出:Father普通代码块–>Father无参构造方法–>Son普通代码块–>Son无参构造函数。随后执行System.out.println(“Father.age”);语句,输出10。

注意点:

java小记_第15张图片
由于遵行先声明先执行的原则,所以在打印语句的时候,由于还没声明age属性,所以会报“非法向前应用”错误。

注意:(静态)变量和(静态)代码块的也是有执行顺序的,与代码书写的顺序一致。在(静态)代码块中可以使用(静态)变量,但是被使用的(静态)变量必须在(静态)代码块前面声明。

即存在以下执行步骤:

1、父类静态变量和静态代码块(先声明的先执行);

2、子类静态变量和静态代码块(先声明的先执行);

3、父类的变量和代码块(先声明的先执行);

4、父类的构造函数;

5、子类的变量和代码块(先声明的先执行);

6、子类的构造函数。

执行时间:

1.父类静态代码块 ( java虚拟机加载类时,就会执行该块代码,故只执行一次)

2 .子类静态代码块 ( java虚拟机加载类时,就会执行该块代码,故只执行一次)

3.父类属性对象初始化

4.父类普通代码块(每次new,每次执行 )

5.父类构造函数(每次new,每次执行)

6.子 类 属性对象初始化

7.子类普通代码块(每次new,每次执行 )

8.子 类构造函数(每次new,每次执行)

总结

当加载Demo类时,JVM会去调用该类父类的静态代码块,父类的静态代码块运行完后去执行子类的静态代码块。当子类的静态代码块执行完毕后JVM又会去找父类中的普通代码块,当父类的普通代码块执行完毕后又会调用父类的构造方法。父类的构造方法执行完毕后,JVM又会去看子类中有没有普通代码块,如果有则执行。执行完毕后,会去调用子类的构造方法。这就是他们之间的顺序
注意:java中所有类都是Object类的子类。当父类中含有有参构造而没有显示的写出无参构造,则子类必须用super关键字显示的调用父类的有参构造,否则编译将不通过。

关于类加载器

java小记_第16张图片

基本集合类的接口继承关系和实现

java小记_第17张图片
java小记_第18张图片

关于重写equals()方法就得要重写hashcode()方法

blog:https://www.shouxicto.com/art...(稍微有点帮助)
blog:https://blog.csdn.net/javazej...
这个问题主要是针对映射相关的操作(Map接口)。学过数据结构的同学都知道Map接口的类会使用到键对象的哈希码,当我们调用put方法或者get方法对Map容器进行操作时,都是根据键对象的哈希码来计算存储位置的,因此如果我们对哈希码的获取没有相关保证,就可能会得不到预期的结果。在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址,这个方法在Object类中声明,因此所有的子类都含有该方法。
————————————————
版权声明:本文为CSDN博主「zejian_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/javazej...
equals()方法的底层源码调用了“==”

public boolean equals(Object obj) {   return (this == obj);     }

为什么?因为是规定。当重写了equals()方法而没重写hashcode()方法时,equals返回的是TRUE,但hashcode()就不一定了,所以为了规定,需要重写,不能违反规定。
java小记_第19张图片

你可能感兴趣的:(java)