java 基础

包装类型

装箱

1
2
// 装箱 调用了 Integer.valueOf(2),将int变成了一个Integer对象
Integer x = 2;

装箱转换是指将一个值类型隐式地转换成一个object 类型,也就是创建一个object 实例并将这个值复制给这个object。

title

拆箱

拆箱转换是指将一个对象类型显式地转换成一个值类型。

1
2
Integer x = 2;     //装箱
int y = x; // 拆箱 调用了 X.intValue()

装箱和拆箱会造成相当大的性能损耗,因此尽量应该避免大量的装箱拆箱操作。

缓存池

new Integer()与Integer.Valueof()区别:

  • new Integer每次都会新建一个对象
  • Integer.Valueof()会复用缓存池中的对象
1
2
3
4
5
6
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true

编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。

1
2
3
//a = b
Integer a = 123;
Integer b = 123;

String

基本类型的变量数据都是存在栈中的,String常量放在常量池里面,String对象放在堆里面。

String常量

String常量存放在常量池里面,常量池中相同的值只有一个。

1
2
3
String s1="hello";
String s2="hello";
System.out.println(s1==s2);//true
  • 第一句代码执行后就在常量池中创建了一个值为hello的String对象;
  • 第二句执行时,因为常量池中存在hello所以就不再创建新的String对象了。
  • 此时该字符串的引用在虚拟机栈里面。
  • 因为s1和s2指向的是同一个对象,所以s1==s2

String对象

String对象的本质是一个不可变的char数组

1
2
3
String a = new String("skj");
String b = new String("skj");
System.out.println(a==b);//false

new String(“skj”)这一步到底做了什么?

  • 在字符串常量池里面创建一个对象,就是”skj”,首先会检查常量池里面有没有这个对象”skj”,没有的话在创建并返回对象的引用,有的话就直接返回这个对象的引用。
  • 在堆上创建一个对象, new String,String对象的本质就是一个char数组,所以String对象中的char数组指向之前返回对象的引用
  • 所以,new String(“skj”)这一句实际上是创建了两个对象,一个在字符串常量池,一个在堆上。

String特性

Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.

  • String是不可变的,因为String的本质是一个final char[],所以String同时又是线程安全的。
  • String由final修饰,是不可以继承的。

字符串拼接问题

1
2
String str0 = "a";
String str1 = str0 + "b";

编译成字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String b
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString()Ljava/lang/String;
22: astore_2
23: return
}

转换成java就是:

1
2
3
4
5
String str0 = "a";
StringBuilder sb = new StringBuilder();
sb.append(str0).append("b");
String str1 = sb.toString();
return str1;

所以,字符串的拼接主要是通过StringBuilder来实现的

要注意的是最后还有toString,返回的是一个String对象

为什么返回的是一个新的String对象呢?

因为String类的char数组是final的,他的指针一旦指向了常量池的某个String,就不可以再改变了.

1
2
3
4
String str0 = "a";
for (int i = 0; i < 10000; i++) {
str0 += "a";
}

当我们在循环体中进行字符串拼接,在循环体里面,每次拼接都会生成一个StringBuilder的临时对象,那么这个程序片段执行下去就会产生10000个StringBuilder的临时对象,这10000个临时对象都是必要的吗?显然不是,我们可以在循环体外直接创建一个StringBuilder对象,然后在循环体中通过append方法拼接字符串,这样就省下了创建并回收10000个临时对象的消耗。

因此,当我们大量使用字符串拼接的时候,还是使用StringBuilder比较好。

拼接示例

  • 使用字符串连接符拼接 : 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”);

img点击并拖拽以移动

  • s2 :这个在编译期间就自动进行了优化的,在常量池中存储一个”second”,并且s2指向它。
  • s12 : JVM对于字符串引用,由于在字符串的”+”连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即("first"+s2)无法被编译器优化,只有在程序运行期来动态分配使用StringBuilder连接后的新String对象赋给s12。
    (编译器创建一个StringBuilder对象,并调用append()方法,最后调用toString()创建新String对象,以包含修改后的字符串内容),常量池中并没有产生新的字符串常量。
  • s3 : 用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
    但是”three”字符串常量在编译期也会被加入到字符串常量池(如果不存在的话)
  • s4 : 同样不能在编译期确定,但是”fo”和”ur”这两个字符串常量也会添加到字符串常量池中,并且在堆中创建String对象。(字符串常量池并不会存放”four”这个字符串)
  • s5 : 原理同s4。

StringBuilder

String的内部实现是一个用final的数组,因此String对象是不可变的,我们每次修改String时,实际上都是new出来了一个新的对象。因此,对于经常进行字符串的修改操作时,String类就需要不断创建新对象,性能极低。StringBuilder内部也是封装的一个字符数组,只不过该数组非final修饰,可以不断修改。所以对于一些经常需要修改字符串的情况,我们应首选StringBuilder

1
2
3
4
5
6
7
8
9
/**
* The value is used for character storage.
*/
char[] value;

/**
* The count is the number of characters used.
*/
int count;

append()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}

我们可以看到,当StringBuilder添加元素的时候,首先判断char[]是否满了,要是满了,Arrays.copyOf对数组进行扩容(返回的是一个新数组)。最后append的方法返回的this,也就是说,与String不同,他并没有创建一个新的对象,主要原因还是char[]不是final的,是可变的,他就可以转换新的指向。

StringBuilder,StringBuffer,String区别

StringBuffer和StringBuilder都继承了抽象类AbstractStringBuilder,这个抽象类和String一样也定义了char[] value和int count,但是与String类不同的是,它们没有final修饰符。因此得出结论:String、StringBuffer和StringBuilder在本质上都是字符数组,不同的是,在进行连接操作时,String每次返回一个新的String实例,而StringBuffer和StringBuilder的append方法直接返回this,所以这就是为什么在进行大量字符串连接运算时,不推荐使用String,而推荐StringBuffer和StringBuilder。那么,哪种情况使用StringBuffe?哪种情况使用StringBuilder呢?

1
2
3
4
5
6
7
8
9
10
public StringBuilder append(String str) {
super.append(str);
return this;
}

public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

区别很明显,StringBuffer加了synchronized关键字,是线程安全的。

为何String要设计成不可变的?

  • 线程安全
  • 字符串常量池的需要。字符串常量池的诞生是为了提升效率和减少内存分配。可以说我们编程有百分之八十的时间在处理字符串,而处理的字符串中有很大概率会出现重复的情况。正因为String的不可变性,常量池很容易被管理和优化。
  • 字符串不变,HashCode也不变,便于缓存Hash Code,不需要重复计算HashCode。

intern()

字符串常量池是在编译期间产生的,通过String的intern()也可以在运行时向字符串常量池放入字符串。

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

简单来说就是intern用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}

/**
输出:false true
**/

分析一下:

  • 先看s3和s4.String s3 = new String("1") + new String("1");,这样,在字符串常量池创建了一个”1”,并且在堆里也创建了一个对象”11”,但在11中是没有对象的。s3.intern(),先去常量池看看有没有”11”,没有,需要在常量池中存储一份”11”,但是在jdk8中常量池已经转移到堆中了,所以可以直接存储堆中的引用(在jdk6之前,常量池还在perm区,就需要再在常量池中存储一份)。所以,s4实际上是指向堆上对象的引用。
  • 再看s1和s2.String s = new String("1");在常量池内已经存储了1,所以s3.intern()啥也没做,s还是指向堆上的对象,s1指向的是常量池的对象。
  • 所以,String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。

String#intern的使用

位运算符

&:按位与

|:按位或

~:异或

^:取反

<<:左移位运算,同理还有右移位运算。

关键字

final

数据:声明数据为常量,一旦初始化之后及不可以改变。

方法:声明方法不可以被重写。

类:声明类不可以被继承。

static

静态变量:类变量,这个变量是属于这个类的,类的所有实例共享,在内存中只存在一份

静态方法:他在类加载的时候就存在了,它不依赖于任何实例,所以static方法必须实现。

static代码块:在类初始化的时候执行一次。

静态成员不可以访问非静态成员,非静态成员可以访问静态成员和非静态成员。

Object方法

equals() and hashCode()

hashcode()返回的是散列值,equals()用来判断两个对象是否等价,所以在重写equals()方法时一定要先重写hashcode()。等价的对象散列值一定相同,但是散列值相同对象不一定等价。

clone()

需要实现Clonable接口并重写clone()方法,才可以实现拷贝。

1
2
3
4
5
6
7
8
9
public class CloneExample implements Cloneable {
private int a;
private int b;

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

浅拷贝

拷贝这个对象的时候,只对基本数据类型进行拷贝,而引用数据类型只是进行了引用的传递,这两个对象还是共享的引用数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ShallowCloneExample implements Cloneable {

private int[] arr;

public ShallowCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}

public void set(int index, int value) {
arr[index] = value;
}

public int get(int index) {
return arr[index];
}

@Override
protected ShallowCloneExample clone() throws CloneNotSupportedException {
return (ShallowCloneExample) super.clone();
}
}


ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222,e1修改了,e2也变了,说明两人引用的是同一个对象

深拷贝

在对引用数据类型拷贝的时候,创建了一个新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class DeepCloneExample implements Cloneable {

private int[] arr;

public DeepCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}

public void set(int index, int value) {
arr[index] = value;
}

public int get(int index) {
return arr[index];
}

@Override
protected DeepCloneExample clone() throws CloneNotSupportedException {
DeepCloneExample result = (DeepCloneExample) super.clone();
result.arr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
result.arr[i] = arr[i];
}
return result;
}
}

DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2

但是,一般来说,不推荐使用clone,可以使用拷贝构造函数来做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CloneConstructorExample {

private int[] arr;

public CloneConstructorExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}

public CloneConstructorExample(CloneConstructorExample original) {
arr = new int[original.arr.length];
for (int i = 0; i < original.arr.length; i++) {
arr[i] = original.arr[i];
}
}

public void set(int index, int value) {
arr[index] = value;
}

public int get(int index) {
return arr[index];
}
}

CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2

反射

反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

当我们的程序在运行时,需要动态的加载一些类这些类可能之前用不到所以不用加载到jvm,而是在运行时根据需要才加载,这样的好处对于服务器来说不言而喻。

举个例子我们的项目底层有时是用mysql,有时用oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了,假设 com.java.dbtest.myqlConnection,com.java.dbtest.oracleConnection这两个类我们要用,这时候我们的程序就写得比较动态化,通过Class tc = Class.forName(“com.java.dbtest.TestConnection”);通过类的全类名让jvm在服务器中找到并加载这个类,而如果是oracle则传入的参数就变成另一个了。

反射使用

获取class对象

1
Class s = Class.forName("java.lang.String");

创建实例

1
s.newInstance();

获取方法

1
2
3
4
5
6
//获取类或接口生命的方法,但不包括继承的方法
public Method[] getDeclaredMethods() throws SecurityException
//获取公有方法
public Method[] getMethods() throws SecurityException
//获取特定的方法,根据参数方法名以及参数类型
public Method getMethod(String name, Class<?>... parameterTypes)

获取变量信息

  • getFiled:访问公有的成员变量
  • getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量

调用方法

通过invoke

1
2
3
4
5
6
7
8
9
10
11
12
public class test1 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);
}
}

访问私有方法和私有变量

甚至可以通过反射访问私有成员。

只需要setAccessible(true)即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void modifyPrivateFiled() throws Exception {
//1. 获取 Class 类实例
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();

//2. 获取私有变量
Field privateField = mClass.getDeclaredField("MSG");

//3. 操作私有变量
if (privateField != null) {
//获取私有变量的访问权
privateField.setAccessible(true);

//修改私有变量,并输出以测试
System.out.println("Before Modify:MSG = " + testClass.getMsg());

//调用 set(object , value) 修改变量的值
//privateField 是获取到的私有变量
//testClass 要操作的对象
//"Modified" 为要修改成的值
privateField.set(testClass, "Modified");
System.out.println("After Modify:MSG = " + testClass.getMsg());
}
}

反射机制

深入解析Java反射

异常

Exception可以通过try catch处理并且使程序恢复。

Error是程序运行时错误,程序会崩溃并且无法恢复。

img

泛型

泛型就是参数化类型,在泛型使用过程中,操作类型的数据类型被定义为一个参数。

泛型最常见的使用是在容器中,我们给容器添加泛型,这样我们可以把所需要的类型作为参数传递给容器,这样,容器就可以接受所有类型的数据,而且同时只能是一个数据,保证了程序的健壮性。

泛型主要有泛型类,泛型接口,泛型方法。

在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;

public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}

public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}

泛型接口

1
2
3
public interface Generator<T> {
public T next();
}

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DataHolder<T>{
T item;

public void setData(T t) {
this.item=t;
}

public T getData() {
return this.item;
}

/**
* 泛型方法
* @param e
*/
public <E> void PrinterInfo(E e) {
System.out.println(e);
}
}
  • public 与返回值中间的E声明这是一个泛型方法,只有声明了才可以使用泛型
  • 没有声明,只是传参的时候使用了泛型,并不是一个泛型方法。
  • 与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 这是一个泛型类
*/
class GenericClassDemo<T> {
/**
* 这个不是泛型方法,只是使用了泛型类中已声明的T
*/
public void show1(T t){
System.out.println(t.toString());
}
/**
* 泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
* 由于下面的泛型方法在声明的时候声明了泛型<E>,因此即使在泛型类中并未声明泛型,
* 编译器也能够正确识别泛型方法中识别的泛型。
*/
public <E> void show2(E e){
System.out.println(e.toString());
}
/**
* 在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型;
* 可以与泛型类中声明的T不是同一种类型。
* show3和show2的E和T只是简单的代指泛型,与泛型类中的T并不是一个
*/
public <T> void show3(T t){
System.out.println(t.toString());
}
}

泛型擦除

1
2
3
4
5
6
7
8
9
10
11
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
System.out.println("----equals----");
}

//输出:----equals----

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段

通配符

上界通配符

<? extends T>,只能放置T以及T的子类。

下界通配符

<? superT>,只能防止T以及T的父类。

无界通配符

<?> ,没有要求。

深入理解泛型

泛型详解