public final class String
implements java.io.Serializable, Comparable, CharSequence {
}
基本特性:
一、声明中是 final 的,不可被继承
二、实现了 Serializable 接口,可以实现序列化
三、实现了 Comparable 接口,使用 compareTo() 方法进行比较,最好别用 '=='
四、StringBuffer、StringBuilder 和 Stringopen in new window,都实现了 CharSequence 接口
五、Java9以前,String 是使用 char[] 实现的,之后改为 byte[] 实现。实现了 String 占用的内存减少一半,同时引入了编码检测开销。
常用方法:
hashCode():返回字符串对应的 hash 值。首先检查是否已经计算过哈希码,如果已经计算过,则直接返回缓存的哈希码。否则,方法将使用31倍哈希法完成计算。(该方法优点在于简单易实现,计算速度快,同时比较均匀分布在哈希表中)
H(s) = (s[0] * 31^(n-1)) + (s[1] * 31^(n-2)) + ... + (s[n-1] * 31^0)
substring():用于截取字符串。
String str = "Hello, world!";
String prefix = str.substring(0, 5); // 提取前5个字符,即 "Hello"
String suffix = str.substring(7); // 提取从第7个字符开始的所有字符,即 "world!"
indexOf():查找一个子字符串(字符)在原字符串中第一次出现的位置,并返回该位置的索引。
String str = "Hello, world!";
int index1 = str.indexOf("o"); // 查找 "o" 子字符串在 str 中第一次出现的位置
int index2 = str.indexOf("o", 5); // 从索引为5的位置开始查找 "o" 子字符串在 str 中第一次出现的位置
System.out.println(index1); // 输出 4
System.out.println(index2); // 输出 8
参考:二哥Java
String s = new String("Java");
这行代码创建了几个对象?
使用 new 关键字创建一个字符串对象时,Java 虚拟机会先在字符串常量池中查找有没有‘Java’这个字符串对象,如果有,就不会在字符串常量池中创建‘Java’这个对象了,直接在堆中创建一个‘Java’的字符串对象,然后将堆中这个‘Java’的对象地址返回赋值给变量 s;如果没有,先在字符串常量池中创建一个‘Java’的字符串对象,然后再在堆中创建一个‘Java’的字符串对象,然后将堆中这个‘Java’的字符串对象地址返回赋值给变量 s。
String s = "Java";
Java 虚拟机会先在字符串常量池中查找有没有“Java”这个字符串对象,如果有,则不创建任何对象,直接将字符串常量池中这个“Java”的对象地址返回,赋给变量 s;如果没有,在字符串常量池中创建“Java”这个对象,然后将其地址返回,赋给变量 s。
参考:美团技术团队解析
Java 7 之前,执行 String.intern()
方法的时候,不管对象在堆中是否已经创建,字符串常量池中仍然会创建一个内容完全相同的新对象; Java 7 之后,由于字符串常量池放在了堆中,执行 String.intern()
方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象了,而是直接保存堆中对象的引用,也就节省了一部分的内存空间。
new String("小萝莉").intern() == "小萝莉" //true
由于字符串是不可变的,遇到字符串拼接,就需要考虑性能问题(尤其是 + )
StringBuffer 操作字符串:加了synchronized 关键字 进行了同步,主要是考虑到多线程环境下的安全问题,所以执行效率会比较低
StringBuilder 操作字符串:在单线程环境下使用,这样效率会高很多。如果要在多线程环境下修改字符串,可以使用 ThreadLocal 来避免多线程冲突
注:当编译器遇到 + 号这个操作符的时候,就会编译为:
new String("cpp") + new String("Java");// 会解析为下面这行
new StringBuilder().append("cpp").append("Java").toString();
"cpp" + "Java"; //这样的话,编译器会直接优化为 "cppJava" 放在字符串常量池
.equals()
就是比较两个对象的内容,内容一致就判定为 true (调用前加入判空)
“==” 操作符就要求必须是同一个对象才为true。
可选方案:
Object.equals():这个静态方法的优势在于不需要调用该方法前判空(常用)
String 类的 .contentEquals():可以将字符串与任何的字符序列(StringBuffer、StringBuilder、String、CharSequence)进行比较
“循环体内,拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符”
原因:循环体内如果用 + 号操作符的话,就会产生大量的 StringBuilder 对象,不仅占用了更多的内存空间,还会让 Java 虚拟机不停的进行垃圾回收,从而降低了程序的性能。
解决方案:循环的外部新建一个 StringBuilder 对象,然后使用 append()
方法将循环体内的字符串添加进来
Java8 编译的时候把 “+” 号操作符替换成了 StringBuilder 的 append() 方法,(+ 号操作符是一种语法糖);Java 9 以后,JDK 用了另外一种方法来动态解释 + 号操作符
和“+” 号操作符相比, concat() 方法在遇到字符串为 null 的时候,会抛出 NullPointerException,而“+”号操作符会把 null 当做是“null”字符串来处理。
拼接的字符串是一个空字符串(""),那么 concat 的效率要更高一点,如果拼接的字符串非常多,concat() 的效率就会下降,因为创建的字符串对象越来越多
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
第一个参数是连接符:
String message = String.join("-", "王二", "太特么", "有趣了");
输出结果为:王二-太特么-有趣了
。
该方法不用担心 NullPointerException。内部使用的仍然是 StringBuilder
StringUtils.join(null) = null
StringUtils.join([]) = ""
StringUtils.join([null]) = ""
StringUtils.join(["a", "b", "c"]) = "abc"
StringUtils.join([null, "", "a"]) = "a"
在拆分之前,先进行检查,判断一下这串字符是否包含该符号 .contains(".") ,否则应该抛出异常,然后再进行拆分 .split("\\.") 等同于 split("[.])
在字符串不是确定的情况下采用正则表达式参考正则表达式文档
面向对象编程
public static void print(String... strs) {
for (String s : strs)
System.out.print(s);
System.out.println();
}
当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法。
注:要避免重载带有可变参数的方法,如果真有需要,就必须在调用方法的时候给出明确的指示
public static void main(String[] args) {
String [] strs = null;
print(strs);
Integer [] ints = null;
print(ints);
}
public static void print(String... strs) {
}
public static void print(Integer... ints) {
}
JNI(Java Native Interface): 它允许 Java 代码和其他语言编写的代码进行交互
缺点:程序不再跨平台,程序不再是绝对安全;
native 用来修饰方法,用 native 声明的方法表示该方法的实现在外部定义,可以用任何语言去实现它,比如说 C/C++。 简单地讲,一个 native Method 就是一个 Java 调用非 Java 代码的接口
复制一个对象可以通过下面三种方式完成:
//需要提供一个这样的构造函数
public Person(Person person) {
this.name = person.name;
this.age = person.age;
}
public static void main(String[] args) {、
Person p1 = new Person("xlin",18);
Person p2 = new Person(p1);
}
提供一个相应的构造函数即可实现
public static void main(String[] args) {、
Person p1 = new Person("xlin",18);
Person p2 = new Person();
p2.name = p1.name;
p2.age = p1.age;
}
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("xlin",18);
Person p2 = (Person) p1.clone();
}
注:参考深拷贝、浅拷贝
了解实例初始化和静态初始化的过程
对象在初始化的时候会先调用构造方法,这是毫无疑问的,只不过,构造方法在执行的时候会把代码初始化块放在构造方法中其他代码之前,所以,先看到了‘代码初始化块’,后看到了‘’构造方法’
从继承的角度看:
class A {
A () {
System.out.println("父类构造方法");
}
}
public class B extends A{
B() {
System.out.println("子类构造方法");
}
{
System.out.println("代码初始化块");
}
public static void main(String[] args) {
new B();
}
}
子类的构造方法在执行的时候会主动去调用父类的构造方法。也就是说,其实是构造方法先执行的,再执行的代码初始化块。
三个规则:
// 实例初始化块
{
System.out.println("执行实例初始化块");
instanceVar = 4;
}
// 静态初始化块
static {
System.out.println("执行静态初始化块");
staticVar = 3;
}
静态初始化块在类加载时执行,只会执行一次,并且优先于实例初始化块和构造方法的执行;实例初始化块在每次创建对象时执行,在构造方法之前执行
Every field declaration in the body of an interface is implicitly public, static, and final.
接口是一个抽象类型,是抽象方法的集合
将一个类定义在另外一个类里面或者一个方法里面,这样的类叫做内部类
class Wanger {
int age = 18;
class Wangxiaoer {
int age = 81;
}
}
成员内部类可以无限制访问外部类的所有成员属性;但外部类想要访问内部类的成员,就不那么容易了,必须先创建一个成员内部类的对象,再通过这个对象来访问;
内部类是依附于外部类,使用起来非常不便
局部内部类是定义在一个方法或者一个作用域里面的类,所以局部内部类的生命周期仅限于作用域内。就好像一个局部变量一样,它是不能被权限修饰符修饰的
public class Wangsan {
public Wangsan print() {
class Wangxiaosan extends Wangsan{
private int age = 18;
}
return new Wangxiaosan();
}
}
主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写。算的上是用的最多的,特别是启动多线程,就像一个方法的参数一样(用完就没了)
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
t.start();
}
}
匿名内部类是唯一一种没有构造方法的类,不允许我们为其编写构造方法,因为它就像是直接通过 new 关键字创建出来的一个对象
静态内部类和成员内部类类似,只是多了一个 static 关键字
public class Wangsi {
static int age;
double money;
static class Wangxxiaosi {
public Wangxxiaosi (){
System.out.println(age);
}
}
}
静态内部类是不允许访问外部类中非 static 的变量和方法
所有类的根类:Object(java.lang.Object)类,所有的类都隐式的继承自 Object 类
Object 类只有一个构造方法,并且是无参构造方法
子类对象实例化父类对象,即父类引用变量指向了子类对象
这时只能使用父类已声明的方法,但方法如果被子类进行重写后,就会去调用子类重写后的方法,否则就调用父类的方法(这就是多态)=> 什么是多态呢?同一个行为具有不同的形态
父类引用变量实际引用必须是子类对象才能成功转型
顺序:
用法:
1. 指向当前对象,调用当前类的方法
2. 调用当前类的构造方法 需要注意的是,this()
必须放在构造方法的第一行
3. 作为参数在方法中传递,也可做为方法的返回值
super()
可以调用父类的构造方法方便在没有创建对象的情况下进行调用,包括变量和方法
注:静态方法不能访问非静态变量和调用非静态方法
被 final 修饰的变量无法重新赋值,所以final 修饰的成员变量必须有一个默认值
final 和 static 一起修饰的成员变量叫做常量,常量名必须全部大写
final Pig pig = new Pig();
//这样就是可以更改 pig 的属性 但pig不可以再指向其他对象
被 final 修饰的方法不能被重写
一个类使用了 final 关键字修饰,那么它就无法被继承,例如 String 类
判断对象是否符合指定的类型,返回 true 表明可以进行类型转换
System.out.println(null instanceof Object);
只有对象才会有 null 值,所以不会报错,但返回值为 false (因为所有对象其实都可以是 null,并不能去判定到底是属于哪一个类)
一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改
1)常量池的需要
字符串常量池是Java堆内存中的一块特殊区域
2)hashCode 需要
字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值
3)线程安全
String 是不可变的,就可以在多个线程之间共享,不需要同步处理
1)确保类是 final 的,不允许被其他类继承。
2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值
3)不要提供任何 setter 方法。
4)如果要修改类的状态,必须返回一个新的对象。
如果一个类有多个名字相同但参数个数不同或参数类型不同的方法,通常称这些方法为方法重载
注:
1. 如果只是返回值类型不同,不算为重载
2. main 方法也可以重载,但系统只认标准写法的 main 方法
如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,方法体可能不同),我们称之为方法重写。 方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件
重写应遵循的规则:
1)只能重写继承过来的方法
2)final、static 的方法不能被重写
3)重写的方法必须有相同的参数列表,相同的返回类型
4)重写的方法不能使用限制等级更严格的权限修饰符
5)重写后的方法不能抛出比父类中更高级别的异常
6)可以在子类中通过 super 关键字来调用父类中被重写的方法
7)构造方法不能被重写
8)如果一个类继承了抽象类,抽象类中的抽象方法必须在子类中被重写
9)synchronized 关键字对重写规则没有任何影响
10)strictfp 关键字对重写规则没有任何影响
属于一种类型,提供了一系列数据用来装饰程序代码。在框架时大量使用
表示一种特殊类型的类,继承自 java.lang.Enum
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象(单例),基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等
1)“==”运算符比较的时候,如果两个对象都为 null,并不会发生 NullPointerException
,而 equals()
方法则会
2)“==”运算符会在编译时进行检查,如果两侧的类型不匹配,提示错误,而 equals()
方法则不会
EnumSet 是一个专门针对枚举类型的 Set 接口的实现类,是处理枚举类型数据的一把利器
EnumMap,是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素
泛型的优秀之处:使用类型参数解决了元素的不确定性
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
实例化泛型类:
Generic genericInteger = new Generic(123456);
public interface Generator {
public T method();
}
1. 实现泛型接口,不指定类型
class GeneratorImpl implements Generator{
@Override
public T method() {
return null;
}
}
2. 实现泛型接口的同时指定类型
class GeneratorImpl implements Generator{
@Override
public String method() {
return "hello";
}
}
public static < E > void printArray( E[] inputArray ) {
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
泛型变量的限定符 extends,可以实现缩小泛型的类型范围
假设有这么三个类:
class Wanglaoer {
public String toString() {
return "王老二";
}
}
class Wanger extends Wanglaoer{
public String toString() {
return "王二";
}
}
class Wangxiaoer extends Wanger{
public String toString() {
return "王小二";
}
}
那么重写一下类:
class Arraylist {
}
当我们向 Arraylist
中添加 Wanglaoer
元素的时候,编译器会提示错误:Arraylist
只允许添加 Wanger
及其子类 Wangxiaoer
对象,不允许添加其父类 Wanglaoer
虚拟机是没有泛型的,将class 文件进行反编译后,类型变量会被取代
通配符使用英文的问号(?)
来表示,在创建一个泛型对象的时候,可以通过 extends 限定子类,通过 super 限定父类
Arraylist extends Wanger> list2 = new Arraylist<>(4);
list2.add(null);
// list2.add(new Wanger()); 不可添加
// list2.add(new Wangxiaoer()); 不可添加
Wanger w2 = list2.get(0);
// Wangxiaoer w3 = list2.get(1);
Arraylist extends Wanger> : list2 是一个 Arraylist 类,其类型是 wang 及其子类
list2 不可通过 add(E e) 的方法添加 Wanger 或者 Wangxiaoer 的对象,唯一例外 null
可以实现赋值:
Arraylist list = new Arraylist<>(4);
Wanger wanger = new Wanger();
list.add(wanger);
Wangxiaoer wangxiaoer = new Wangxiaoer();
list.add(wangxiaoer);
Arraylist extends Wanger> list2 = list;//将 list 的值赋予了 list2
// 此时 list2 == list
Wanger w2 = list2.get(1);
System.out.println(w2);
System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(new Wangxiaoer()));
list2 不允许往其添加其他元素,所以此时它是安全的
可以向 Arraylist 中存入父类为 Wanger 的对象,无法取出数据
Arraylist super Wanger> list3 = new Arraylist<>(4);
list3.add(new Wanger()); // 可以存数据
list3.add(new Wangxiaoer());
// Wanger w3 = list3.get(0); 无法取出数据
缺点:
应用场景:
例子:
public class Writer {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这是一个类,通过反射创建对象:
public class ReflectionDemo1 {
public static void main(String[] args) throws
ClassNotFoundException,
NoSuchMethodException,
IllegalAccessException,
InvocationTargetException,
InstantiationException {
Class clazz = Class.forName("com.it.Writer");
Constructor constructor = clazz.getConstructor();
Object object = constructor.newInstance();
Method setNameMethod = clazz.getMethod("setName", String.class);
setNameMethod.invoke(object, "xlin");
Method getNameMethod = clazz.getMethod("getName");
System.out.println(getNameMethod.invoke(object));
}
}
Class clazz = Class.forName("com.it.Writer");
Class 对象是一种特殊的对象,它代表了程序中的类和接口
Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成
Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)
通过类的全名获取 Class 对象,还有以下两种方式:
Constructor constructor = clazz.getConstructor();
Object object = constructor.newInstance();
Method setNameMethod = clazz.getMethod("setName", String.class);
Method getNameMethod = clazz.getMethod("getName");
setNameMethod.invoke(object, "xlin");
getNameMethod.invoke(object)
Class.forName(),参数为反射类的完全限定名
Class c1 = Class.forName("com.it.ReflectionDemo3");
System.out.println(c1.getCanonicalName());
// com.it.ReflectionDemo3
Class c2 = Class.forName("[D");
System.out.println(c2.getCanonicalName());
// double[]
Class c3 = Class.forName("[[Ljava.lang.String;");
System.out.println(c3.getCanonicalName());
// java.lang.String[][]
类名 + .class,只适合在编译前就知道操作的 Class
Class c1 = ReflectionDemo3.class;
System.out.println(c1.getCanonicalName());
//com.it.ReflectionDemo3
Class c2 = String.class;
System.out.println(c2.getCanonicalName());
//java.lang.String
Class c3 = int[][][].class;
System.out.println(c3.getCanonicalName());
//int[][][]
Class c1 = Writer.class;
Writer writer = (Writer) c1.newInstance();
Class c2 = Class.forName("com.it.Writer");
Constructor constructor = c2.getConstructor();
Object object = constructor.newInstance();
注:在 Spring 里有个常用的
Class> aClass = classLoader.loadClass(classFullName);
Class clazz = Class.forName(classFullName);
两者的区别:
下面的方式会调用该类的静态方法;上面的方式则不会,是一种轻量级的;
Class c2 = Class.forName("com.it.Writer");
Constructor constructor = c2.getConstructor();
Constructor[] constructors1 = String.class.getDeclaredConstructors();
for (Constructor c : constructors1) {
System.out.println(c);
}
把关键字 Constructor 换成 Field 即可
Field name = clazz.getField("name");
把关键字 Constructor 换成 Method 即可
Method setNameMethod = clazz.getMethod("setName", String.class);
Method getNameMethod = clazz.getMethod("getName");
Method[] methods1 = System.class.getDeclaredMethods();
Method[] methods2 = System.class.getMethods();
参考:大白话说Java反射:入门、使用、原理