代码块
静态代码块
静态代码块的格式:
static { }
- 静态代码块特点:
- 必须有static修饰
- 会与类一起优先加载,且自动触发执行一次
- 只能访问静态资源
- 静态代码块作用:
- 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化
- 先执行静态代码块,在执行main函数里的操作
public class CodeDemo { public static String schoolName ; public static ArrayList<String> lists = new ArrayList<>(); // 静态代码块,属于类,与类一起加载一次! static { System.out.println("静态代码块被触发执行~~~~~~~"); // 在静态代码块中进行静态资源的初始化操作 schoolName = "张三"; lists.add("3"); lists.add("4"); lists.add("5"); } public static void main(String[] args) { System.out.println("main方法被执行"); System.out.println(schoolName); System.out.println(lists); } } /*静态代码块被触发执行~~~~~~~ main方法被执行 张三 [3, 4, 5] */
实例代码块
实例代码块的格式:
{ }
-
实例代码块的特点:
- 无static修饰,属于对象
- 会与类的对象一起加载,每次创建类的对象的时候,实例代码块都会被加载且自动触发执行一次
- 实例代码块的代码在底层实际上是提取到每个构造器中去执行的
-
实例代码块的作用:实例代码块可以在创建对象之前进行实例资源的初始化操作
public class CodeDemo { private String name; private ArrayList<String> lists = new ArrayList<>(); { name = "代码块"; lists.add("java"); System.out.println("实例代码块被触发执行一次~~~~~~~~"); } public CodeDemo02(){ }//构造方法 public CodeDemo02(String name){} public static void main(String[] args) { CodeDemo c = new CodeDemo();//实例代码块被触发执行一次 System.out.println(c.name); System.out.println(c.lists); new CodeDemo02();//实例代码块被触发执行一次 } }
API
Object
基本介绍
Object 类是 Java 中的祖宗类,一个类或者默认继承 Object 类,或者间接继承 Object 类,Object 类的方法是一切子类都可以直接使用
Object 类常用方法:
-
public String toString()
:默认是返回当前对象在堆内存中的地址信息:类的全限名@内存地址,例:Student@735b478;- 直接输出对象名称,默认会调用 toString() 方法,所以省略 toString() 不写;
- 如果输出对象的内容,需要重写 toString() 方法,toString 方法存在的意义是为了被子类重写
-
public boolean equals(Object o)
:默认是比较两个对象的引用是否相同 -
protected Object clone()
:创建并返回此对象的副本
只要两个对象的内容一样,就认为是相等的:
public boolean equals(Object o) { // 1.判断是否自己和自己比较,如果是同一个对象比较直接返回true if (this == o) return true; // 2.判断被比较者是否为null ,以及是否是学生类型。 if (o == null || this.getClass() != o.getClass()) return false; // 3.o一定是学生类型,强制转换成学生,开始比较内容! Student student = (Student) o; return age == student.age && sex == student.sex && Objects.equals(name, student.name); }
面试题:== 和 equals 的区别
- == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作
- Object 类中的方法,默认比较两个对象的引用,重写 equals 方法比较的是两个对象的内容是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象
hashCode 的作用:
- hashCode 的存在主要是用于查找的快捷性,如 Hashtable,HashMap 等,可以在散列存储结构中确定对象的存储地址
- 如果两个对象相同,就是适用于 equals(java.lang.Object) 方法,那么这两个对象的 hashCode 一定要相同
- 哈希值相同的数据不一定内容相同,内容相同的数据哈希值一定相同
深浅克隆
深浅拷贝(克隆)的概念:
-
浅拷贝 (shallowCopy):对基本数据类型进行值传递,对引用数据类型只是复制了引用,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址
Java 中的复制方法基本都是浅克隆:Object.clone()、System.arraycopy()、Arrays.copyOf()
-
深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误
Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法
Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常
-
Clone & Copy:
Student s = new Student
Student s1 = s
:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 object,对对象的修改会影响对方Student s2 = s.clone()
:会生成一个新的Student对象,并且和s具有相同的属性值和方法 -
Shallow Clone & Deep Clone:
浅克隆:Object 中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy
- 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可
- 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量
深克隆:在对整个对象浅克隆后,对其引用变量进行克隆,并将其更新到浅克隆对象中去
public class Student implements Cloneable{ private String name; private Integer age; private Date date; @Override protected Object clone() throws CloneNotSupportedException { Student s = (Student) super.clone(); s.date = (Date) date.clone(); return s; } //..... }
SDP → 创建型 → 原型模式
Objects
Objects 类与 Object 是继承关系。
Objects的方法:
-
public static boolean equals(Object a, Object b)
: 比较两个对象是否相同。 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!!public static boolean equals(Object a, Object b) { return a == b || a != null && a.equals(b); }
-
public static boolean isNull(Object obj)
: 判断变量是否为null ,为null返回true ,反之! -
public static String toString(对象)
: 返回参数中对象的字符串表示形式 -
public static String toString(对象, 默认字符串)
: 返回对象的字符串表示形式。
public class ObjectsDemo { public static void main(String[] args) { Student s1 = null; Student s2 = new Student(); System.out.println(Objects.equals(s1 , s2));//推荐使用 // System.out.println(s1.equals(s2)); // 空指针异常 System.out.println(Objects.isNull(s1)); System.out.println(s1 == null);//直接判断比较好 } } public class Student { }
String
基本介绍
String 被声明为 final,因此不可被继承 (Integer 等包装类也不能被继承)
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final byte[] value; /** The identifier of the encoding used to encode the bytes in {@code value}. */ private final byte coder; }
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder
来标识使用了哪种编码
value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组,并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变,也保证线程安全
注意:不能改变的意思是每次更改字符串都会产生新的对象,并不是对原始对象进行改变
String s = "abc"; s = s + "cd"; //s = abccd 新对象
常用方法
常用 API:
-
public boolean equals(String s)
: 比较两个字符串内容是否相同、区分大小写 -
public boolean equalsIgnoreCase(String anotherString)
: 比较字符串的内容,忽略大小写 -
public int length()
: 返回此字符串的长度 -
public String trim()
: 返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 -
public String[] split(String regex)
: 将字符串按给定的正则表达式分割成字符串数组 -
public char charAt(int index)
: 取索引处的值 -
public char[] toCharArray()
: 将字符串拆分为字符数组后返回 -
public boolean startsWith(String prefix)
: 测试此字符串是否以指定的前缀开头 -
public int indexOf(String str)
: 返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 -
public int lastIndexOf(String str)
: 返回字符串最后一次出现str的索引,没有返回 -1 -
public String substring(int beginIndex)
: 返回子字符串,以原字符串指定索引处到结尾 -
public String substring(int i, int j)
: 指定索引处扩展到 j - 1 的位置,字符串长度为 j - i -
public String toLowerCase()
: 将此 String 所有字符转换为小写,使用默认语言环境的规则 -
public String toUpperCase()
: 使用默认语言环境的规则将此 String 所有字符转换为大写 -
public String replace(CharSequence target, CharSequence replacement)
: 使用新值,将字符串中的旧值替换,得到新的字符串
String s = 123-78; s.replace("-","");//12378
构造方式
构造方法:
-
public String()
: 创建一个空白字符串对象,不含有任何内容 -
public String(char[] chs)
: 根据字符数组的内容,来创建字符串对象 -
public String(String original)
: 根据传入的字符串内容,来创建字符串对象
直接赋值:String s = "abc"
直接赋值的方式创建字符串对象,内容就是 abc
- 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,返回堆内存中对象的引用
- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会在 String Pool 中创建一个字符串对象,并在字符串池中维护
String str = new String("abc")
创建字符串对象:
- 创建一个对象:字符串池中已经存在 abc 对象,那么直接在创建一个对象放入堆中,返回堆内引用
- 创建两个对象:字符串池中未找到 abc 对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用 equals()
new String("a") + new String("b")
创建字符串对象:
-
对象 1:new StringBuilder()
-
对象 2:new String("a")、对象 3:常量池中的 a
-
对象 4:new String("b")、对象 5:常量池中的 b
-
StringBuilder 的 toString():
@Override public String toString() { return new String(value, 0, count); }
- 对象 6:new String("ab")
- StringBuilder 的 toString() 调用,在字符串常量池中没有生成 ab,new String("ab") 会创建两个对象因为传参数的时候使用字面量创建了对象 ab,当使用数组构造 String 对象时,没有加入常量池的操作
String Pool
基本介绍
字符串常量池(String Pool / StringTable / 串池)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,常量池类似于 Java 系统级别提供的缓存,存放对象和引用
- StringTable,类似 HashTable 结构,通过
-XX:StringTableSize
设置大小,JDK 1.8 中默认 60013 - 常量池中的字符串仅是符号,第一次使用时才变为对象,可以避免重复创建字符串对象
- 字符串变量的拼接的原理是 StringBuilder#append,append 方法比字符串拼接效率高(JDK 1.8)
- 字符串常量拼接的原理是编译期优化,拼接结果放入常量池
- 可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中
intern()
JDK 1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中:
- 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收)
- 不存在,会把对象的引用地址复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象
JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回
public class Demo { // 常量池中的信息都加载到运行时常量池,这时a b ab是常量池中的符号,还不是java字符串对象,是懒惰的 // ldc #2 会把 a 符号变为 "a" 字符串对象 ldc:反编译后的指令 // ldc #3 会把 b 符号变为 "b" 字符串对象 // ldc #4 会把 ab 符号变为 "ab" 字符串对象 public static void main(String[] args) { String s1 = "a"; // 懒惰的 String s2 = "b"; String s3 = "ab"; // 串池 String s4 = s1 + s2; // 返回的是堆内地址 // 原理:new StringBuilder().append("a").append("b").toString() new String("ab") String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab System.out.println(s3 == s4); // false System.out.println(s3 == s5); // true String x2 = new String("c") + new String("d"); // new String("cd") // 虽然 new,但是在字符串常量池没有 cd 对象,toString() 方法 x2.intern(); String x1 = "cd"; System.out.println(x1 == x2); //true } }
- == 比较基本数据类型:比较的是具体的值
- == 比较引用数据类型:比较的是对象地址值
结论:
String s1 = "ab"; // 仅放入串池 String s2 = new String("a") + new String("b"); // 仅放入堆 // 上面两条指令的结果和下面的 效果 相同 String s = new String("ab");
面试问题
问题一:
public static void main(String[] args) { String s = new String("a") + new String("b");//new String("ab") //在上一行代码执行完以后,字符串常量池中并没有"ab" String s2 = s.intern(); //jdk6:串池中创建一个字符串"ab" //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向new String("ab"),将此引用返回 System.out.println(s2 == "ab");//jdk6:true jdk8:true System.out.println(s == "ab");//jdk6:false jdk8:true }
问题二:
public static void main(String[] args) { String str1 = new StringBuilder("58").append("tongcheng").toString(); System.out.println(str1 == str1.intern());//true,字符串池中不存在,把堆中的引用复制一份放入串池 String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2 == str2.intern());//false }
原因:
-
System 类当调用 Version 的静态方法,导致 Version 初始化:
private static void initializeSystemClass() { sun.misc.Version.init(); }
-
Version类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的"java"字符串字面量就被放入的字符串常量池:
package sun.misc; public class Version { private static final String launcher_name = "java"; private static final String java_version = "1.8.0_221"; private static final String java_runtime_name = "Java(TM) SE Runtime Environment"; private static final String java_profile_name = ""; private static final String java_runtime_version = "1.8.0_221-b11"; //... }
内存位置
Java 7之前,String Pool 被放在运行时常量池中,它属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致OutOfMemoryError 错误
演示 StringTable 位置:
-
-Xmx10m
设置堆内存10m -
在jdk8下设置:
-Xmx10m -XX:-UseGCOverheadLimit
(运行参数在Run Configurations VM options) -
在jdk6下设置:
-XX:MaxPermSize=10m
public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<String>(); int i = 0; try { for (int j = 0; j < 260000; j++) { list.add(String.valueOf(j).intern()); i++; } } catch (Throwable e) { e.printStackTrace(); } finally { System.out.println(i); } }
优化常量池
两种方式:
-
调整 -XX:StringTableSize=桶个数,数量越少,性能越差
-
intern 将字符串对象放入常量池,通过复用字符串的引用,减少内存占用
/** * 演示 intern 减少内存占用 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000 */ public class Demo1_25 { public static void main(String[] args) throws IOException { List<String> address = new ArrayList<>(); System.in.read(); for (int i = 0; i < 10; i++) { //很多数据 try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) { String line = null; long start = System.nanoTime(); while (true) { line = reader.readLine(); if(line == null) { break; } address.add(line.intern()); } System.out.println("cost:" +(System.nanoTime()-start)/1000000); } } System.in.read(); } }
不可变好处
- 可以缓存 hash 值,例如 String 用做 HashMap 的 key,不可变的特性可以使得 hash 值也不可变,只要进行一次计算
- String Pool 的需要,如果一个 String 对象已经被创建过了,就会从 String Pool 中取得引用,只有 String 是不可变的,才可能使用 String Pool
- 安全性,String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是
- String 不可变性天生具备线程安全,可以在多个线程中安全地使用
- 防止子类继承,破坏 String 的 API 的使用
StringBuilder
String StringBuffer 和 StringBuilder 区别:
- String : 不可变的字符序列,线程安全
- StringBuffer : 可变的字符序列,线程安全,底层方法加 synchronized,效率低
- StringBuilder : 可变的字符序列,JDK5.0 新增;线程不安全,效率高
相同点:底层使用 char[] 存储
构造方法:
-
public StringBuilder()
:创建一个空白可变字符串对象,不含有任何内容 -
public StringBuilder(String str)
:根据字符串的内容,来创建可变字符串对象
常用API :
-
public StringBuilder append(任意类型)
:添加数据,并返回对象本身 -
public StringBuilder reverse()
:返回相反的字符序列 -
public String toString()
:通过 toString() 就可以实现把 StringBuilder 转换为 String
存储原理:
String str = "abc"; char data[] = {'a', 'b', 'c'}; StringBuffer sb1 = new StringBuffer();//new byte[16] sb1.append('a'); //value[0] = 'a';
append 源码:扩容为二倍
public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; } private void ensureCapacityInternal(int minimumCapacity) { // 创建超过数组长度就新的char数组,把数据拷贝过去 if (minimumCapacity - value.length > 0) { //int newCapacity = (value.length << 1) + 2;每次扩容2倍+2 value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } } public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { // 将字符串中的字符复制到目标字符数组中 // 字符串调用该方法,此时value是字符串的值,dst是目标字符数组 System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); }