静态变量可以被类的所有实例共享。无论一个类创建了多少个对象,它们都共享同一份静态变量。通常情况下,静态变量会被 final 关键字修饰成为常量。
这个需要结合 JVM 的相关知识,主要原因如下:
在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理,重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 printVariable 方法就可以接受 0 个或者多个参数。
public static void method1(String... args) {//......
}
Java 中有 8 种基本数据类型,分别为:6 种数字类型: 4 种整数型:byte、short、int、long2 种浮点型:float、double1 种字符类型:char1 种布尔型:boolean。
jdk1.5以后不再需要通过valueOf()的方式手动装箱,采用自动装箱的方式,其实底层用的还是valueOf()方法,只是现在不用要手动执行了,是通过编译器调用,执行时会自动生成一个静态数组作为缓存,例如Integer 默认对应的缓存数组范围在[-128,127],只要数据在这个范围内,就可以从缓存中拿到相应的对象。超出范围就新建对象,这个就是缓存机制。
源码
valueOf 方法
public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i);
}
Integer 的内部类 IntegerCache
private static class IntegerCache {static final int low = -128;static final int high;static final Integer cache[];static {// high value may be configured by propertyint h = 127;String integerCacheHighPropValue =VM.getSavedProperty("java.lang.Integer.IntegerCache.high");if (integerCacheHighPropValue != null) {try {int i = parseInt(integerCacheHighPropValue);i = Math.max(i, 127);// Maximum array size is Integer.MAX_VALUEh = Math.min(i, Integer.MAX_VALUE - (-low) -1);} catch( NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}}high = h;cache = new Integer[(high - low) + 1];int j = low;for(int k = 0; k < cache.length; k++)cache[k] = new Integer(j++);// range [-128, 127] must be interned (JLS7 5.1.7)assert IntegerCache.high >= 127;}private IntegerCache() {}
}
IntegerCache是Integer的静态内部类,valueOf()调用的IntegerCache.cache就是一个数组对象,数组的大小取决于范围内的最大值和最小值,例如上面的Integer是[-128,127]。然后数组内的元素都会被赋一个Integer对象,缓存也就形成了。
存在数组缓存,也就意味着,如果取值在[-128,127],使用valueOf()或者自动装箱创建的Integer对象都是在数组中取出,因此对象指向的内存地址是完全一样的。而如果用new或者是超出这个范围都要重新创建对象。
其它类型缓存范围
Byte:(全部缓存)
Short:(-128 — 127缓存)
Integer:(-128 — 127缓存)
Long:(-128 — 127缓存)
Float:(没有缓存)
Double:(没有缓存)
Boolean:(全部缓存)
Character:(0 — 127缓存)
测试
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println(a == b); //new创建的两个对象,即使值相同,指向的内存地址也是不同的,使用==进行比较,比较的是地址,返回结果为false
Integer c = 1;
Integer d = 1;
System.out.println(c == d); //自动装箱和缓存机制,两个对象实际上是相同的,返回结果为true
Integer e = 128;
Integer f = 128;
System.out.println(e == f); //超出缓存范围,执行时会new新对象,两个对象不同,返回结果为false结果
false
true
false
什么是自动拆装箱
1.5 以前需要调用 valueOf() 方法手动装箱。使用 intValue() ,doubleValue() 等这类的方法手动拆箱。
1.5 以后才有自动装箱和拆箱,自动装箱时,编译器调用 valueOf 将原始类型值转换成对象。自动拆箱时,编译器通过调用类似intValue(),doubleValue()等这类的方法将对象转换成原始类型值。
浮点数运算精度丢失代码演示:
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
为什么会出现这个问题呢?
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
就比如说十进制下的 0.2 就没办法精确转换成二进制小数:
// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。在 Java 中,64 位 long 整型是最大的整数类型。
Java中当一个数的超过long型范围(能够表示64位的整数)时可以使用BigInteger和BigDecimal类型。
public class Main {public static void main(String[] args) {BigInteger bigInteger1=new BigInteger("333333333333");BigInteger bigInteger2=new BigInteger("444444444444");System.out.println("两个数分别是"+bigInteger1+"和"+bigInteger2);//BigInteger类型数字的四则运算//加System.out.println(bigInteger1.add(bigInteger2));//减System.out.println(bigInteger1.subtract(bigInteger2));//乘System.out.println(bigInteger1.multiply(bigInteger2));//除System.out.println(bigInteger1.divide(bigInteger2));}
}
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
对象的相等一般比较的是内存中存放的内容是否相等。
引用相等一般比较的是他们指向的内存地址是否相等。
Java也支持面向对象的三大特征:封装、继承和多态。
封装
java中提供了不同的封装级别:public、protected、默认的、private。
继承
提供了extends关键字来让子类继承父类,子类继承父类就可以继承到父类的Field和方法,如果访问控制允许,子类实例可以直接调用父类里定义的方法。继承是实现类复用的重要手段,除此之外,也可通过组合关系来实现这种复用,从某种程度上来看,继承和组合具有相同的功能。使用继承关系来实现复用时,子类对象可以直接赋给父类变量,这个变量具有多态性,编程更加灵活;而利用组合关系来实现复用时,则不具备这种灵活性。
多态
对面向对象来说,多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是大家通常所说的多态性。
Java 实现多态有 3 个必要条件:继承、重写和向上转型。只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。
通过父类型的引用,调用父类型的方法,如果子类型重写了父类型的方法,父类型的引用指向了子类的对象,调用该方法就具备多态性。在程序中的多态,就是指同一个引用类型,使用不同的实例而执行不同的操作。
多态的意义:一个类型的引用指向不同的对象,会有不同的功能实现同一个对象,造型成不同的类型,会有不同的功能。
共同点 :
区别 :
浅拷贝:克隆出来的数据并不能完全脱离原数据,克隆前与克隆后的变量各自的变化会相互影响。这是因为引用变量存储在栈中,而实际的对象存储在堆中。每一个引用变量都有一根指针指向其堆中的实际对象。即当一个变量值改变时,另一个变量也会跟着发生变化。
深拷贝:所有元素或属性均完全复制,与原对象完全脱离,也就是说所有对于新对象的修改都不会反映到原对象中。这是因为原始变量之间的赋值操作本质上就是当一个原始变量把值赋给另一个原始变量时,只是把栈中的内容复制给另一个原始变量,在这种操作下,引用变量指向的将不再是堆中的同一块地址,因此对于新对象的修改并不会影响到原对象。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
一张图来描述浅拷贝、深拷贝、引用拷贝:
/*** native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。*/
public final native Class> getClass()
/*** native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。*/
public native int hashCode()
/*** 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。*/
public boolean equals(Object obj)
/*** naitive 方法,用于创建并返回当前对象的一份拷贝。*/
protected native Object clone() throws CloneNotSupportedException
/*** 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。*/
public String toString()
/*** native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。*/
public final native void notify()
/*** native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。*/
public final native void notifyAll()
/*** native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。*/
public final native void wait(long timeout) throws InterruptedException
/*** 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。*/
public final void wait(long timeout, int nanos) throws InterruptedException
/*** 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念*/
public final void wait() throws InterruptedException
/*** 实例被垃圾回收器回收的时候触发的操作*/
protected void finalize() throws Throwable { }
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
equals() 方法存在两种使用情况:
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true
我们以 HashSet 如何检查重复 为例子来说明为什么要有 hashCode
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
其实, hashCode() 和 equals()都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢
这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode() 方法呢
那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的,因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。
总结下来就是 :
String 是不可变的,StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
String 类中使用 final 关键字修饰字符数组来保存字符串
public final class String implements java.io.Serializable, Comparable, CharSequence {private final char value[];//...
}
String 真正不可变有下面几点原因:
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
上面的代码对应的字节码如下:
可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {s += arr[i];
}
System.out.println(s);
StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。
如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {s.append(value);
}
System.out.println(s);
String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的 equals 方法是比较的对象的内存地址。
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。
String s1 = new String("abc");
2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
// 字符串常量池中已存在字符串对象“abc”的引用
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
String s2 = new String("abc");
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化,常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string"; 。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
String str4 = new StringBuilder().append(str1).append(str2).toString();
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
被 final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
示例代码(str2 在运行时才能确定其值):
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {return "ing";
}
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException…。
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
try {System.out.println("Try to do something");throw new RuntimeException("RuntimeException");
} catch (Exception e) {System.out.println("Catch Exception -> " + e.getMessage());
} finally {System.out.println("Finally");
}
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
不一定的!在某些情况下,finally 中的代码不会被执行。就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
try {System.out.println("Try to do something");throw new RuntimeException("RuntimeException");
} catch (Exception e) {System.out.println("Catch Exception -> " + e.getMessage());// 终止当前正在运行的Java虚拟机System.exit(1);
} finally {System.out.println("Finally");
}
另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:
throw 作用在方法内,表示抛出具体异常,throws 作用在方法的声明上,表示方法抛出异常,由调用者来进行异常处理。
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList persons = new ArrayList() 这行代码就指明了该 ArrayList 对象只能传入 Person 对象,如果传入其他类型的对象就会报错。
ArrayList extends AbstractList
并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
//此处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();
}
实现泛型接口,不指定类型:
class GeneratorImpl implements Generator{@Overridepublic T method() {return null;}
}
实现泛型接口,指定类型:
class GeneratorImpl implements Generator{@Overridepublic 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 );
注意: public static < E > void printArray( E[] inputArray ) 一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的
运行期间动态获取类中的信息(属性,方法,包的信息,注解等)以及动态调用对象的方法和属性的功能,称之为java语言的反射机制,通俗的理解,就是在运行期间对类的内容进行操作。
https://blog.csdn.net/yy139926/article/details/124831677
Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。注解本质是一个继承了Annotation 的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}public interface Override extends Annotation{}
JDK 提供了很多内置的注解(比如 @Override 、@Deprecated),同时,我们还可以自定义注解。
注解只有被解析之后才会生效,常见的解析方法有两种:
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
下面是序列化和反序列化常见应用场景:
综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?
因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
对于不想进行序列化的变量,使用 transient 关键字修饰。transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 transient 还有几点注意:
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:
IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
1、强引用
Java中默认声明的就是强引用,比如:
Object obj = new Object();
obj = null;
只要强引用存在,垃圾回收器将永远不会回收被引用的对象。如果想被回收,可以将对象置为null
2、软引用
在内存足够的时候,软引用不会被回收,只有在内存不足时,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会跑出内存溢出异常。
byte[] buff = new byte[1024 * 1024];
SoftReference sr = new SoftReference<>(buff);
3、弱引用
进行垃圾回收时,弱引用就会被回收。
4、虚引用(PhantomReference)
5、引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用、虚引用一起配合使用。当垃圾回收器准备回收一个对象时,如果发现它还有引用,就会在回收对象之前,把这个引用加入到引用队列中。程序可以通过判断引用队列中是否加入了引用,来判断被引用的对象是否将要被垃圾回收,这样可以在对象被回收之前采取一些必要的措施。
网络IO模型有BIO、NIO、AIO
首先在网络编程中,客户端给服务端发送消息大约分为两个个步骤。
在BIO中每一个连接都需要分配一个线程来执行,假如A客户端连接了服务器,但是还没有发送消息,这个时候B客户端向服务器发送连接请求,这个时候服务器是没有办法处理B客户端的连接请求的。
因为一个线程处理了一个客户端的连接后就阻塞住,并等待处理该客户端发送过来的数据。处理完该客户端的数据后才能处理其他客户端的连接请求。
那你这个是只有一个线程的时候,那我弄多个线程不就好了,来一个请求连接我弄一个线程?那假如有一万个连接请求同时过来,那你开启一万个线程服务端不就崩了嘛!那我弄一个线程池呢,我最大线程数最多弄500呢?那假如有500线程只请求连接,并不发送数据呢,那你这个线程池不也一样废了吗。这500个请求连接上了还没有发送数据,那么线程池的500个线程就没办法去处理别的请求,这样照样废废了。
那咋办呢?
可以使用NIO同步非阻塞,这样就不需要很多线程,一个线程也能处理很多的请求连接和请求数据。NIO他是怎么实现一个线程处理多个连接请求和多个请求数据的呢?NIO会将获取的请求连接放入到一个数组中,然后再遍历这个数据查看这些连接有没有数据发送过来。
但是有个问题啊,如果B和C只连接了,但是一直没有发送数据,那每次还循环判断他俩有没有发送数据的请求是不是有点多余了,能不能在我知道B和C肯定发送了数据的情况下再去遍历他呢?可以引入Epoll,在JDK1.5开始引入了epoll通过事件响应来优化NIO,原理是客户端的每一次连接和每一次发送数据都看作是一个事件,每次发生事件会注册到服务端的一个集合中去,然后客户端只需要遍历这个集合就可以了。
那AIO有什么特点呢?
AIO是异步非阻塞,他对于客户端的连接请求和发送数据请求是用不同的线程来处理的,他是通过回调来通知服务端程序去启动线程处理,适用于长连接的场景。
当程序中用到的数值位数特别多时,为了解决这种问题,Java 7引入了一个新功能:程序员可以在数值中使用下画线,不管是整型数值,还是浮点型数值,都可以自由地使用下画线。通过使用下画线分隔,可以更直观地分辨数值常量中到底包含多少位。如double a = 3.14_15_92_65_36。
final可以修饰类,变量,方法,修饰的类不能被继承,修饰的变量不能重新赋值,修饰的方法不能被重写。
finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。
finalize方法用于垃圾回收。finalize() Object 类中定义的方法,Java 中允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集 器在销毁对象时调用的,通过重写 finalize() 方法可以整理系统资源或者执行其他清理工作。
byte的范围是-128~127。
字节长度为8位,最左边的是符号位,而127的二进制为01111111,所以执行+1操作时,01111111变为10000000。
大家知道,计算机中存储负数,存的是补码的兴衰。左边第一位为符号位。
那么负数的补码转换成十进制如下:
一个数如果为正,则它的原码、反码、补码相同;一个正数的补码,将其转化为十进制,可以直接转换。
已知一个负数的补码,将其转换为十进制数,步骤如下:
先对各位取反;将其转换为十进制数;加上负号,再减去1;
例如10000000,最高位是1,是负数,①对各位取反得01111111,转换为十进制就是127,加上负号得-127,再减去1得-128;
方法的重写要遵循 “两同两小一大” 规则。
“两同” :方法名相同、形参列表相同。
“两小” :指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等。
“一大” :指的是子类方法的访问权限应比父类方法的访问权限更大或相等。尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
boolean result = obj instanceof class
其中 obj 为一个对象,Class 表示一个类或者一个接口。
当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
使用实体类作为入参的优点
使用实体类作为入参的缺点
使用Map作为入参的优点
使用Map作为入参的缺点
public static void main(String[] args) {int yang = 1;System.out.println("yang调用前" + yang);yang(yang);System.out.println("yang调用后" + yang);
}public static void yang(int x) {System.out.println("yang中 赋值前" + x);x = 2;System.out.println("yang中 赋值后" + x);
}结果yang调用前1
yang中 赋值前1
yang中 赋值后2
yang调用后1
public static void main(String[] args) {int[] yang = new int[] {1, 2};System.out.println("yang调用前" + Arrays.toString(yang));yang(yang);System.out.println("yang调用后" + Arrays.toString(yang));
}public static void yang(int[] x) {System.out.println("yang中 赋值前" + Arrays.toString(x));int temp = x[0];x[0] = x[x.length - 1];x[x.length - 1] = temp;System.out.println("yang中 赋值后" + Arrays.toString(x));
}结果
yang调用前[1, 2]
yang中 赋值前[1, 2]
yang中 赋值后[2, 1]
yang调用后[2, 1]
public static void main(String[] args) {int[] yang = new int[] {1, 2};System.out.println("yang调用前" + Arrays.toString(yang));yang(yang);System.out.println("yang调用后" + Arrays.toString(yang));
}public static void yang(int[] x) {x = new int[] {7, 8};System.out.println("yang中 赋值前" + Arrays.toString(x));int temp = x[0];x[0] = x[x.length - 1];x[x.length - 1] = temp;System.out.println("yang中 赋值后" + Arrays.toString(x));
}结果
yang调用前[1, 2]
yang中 赋值前[7, 8]
yang中 赋值后[8, 7]
yang调用后[1, 2]
当Java程序直接使用形如"hello"的字符串直接量时,JVM将会使用常量池来管理这些字符串,当使用new String(“hello”)时,JAVA虚拟机首先在字符串池中查找是否已经存在了值为“hello”的这么一个对象,它的判断依据是String类equals()方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用,如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。然后把常量池中的地址放到堆内存中,然后在栈中创建一个引用指向其堆内存块对象(此过程中可能会创建两个对象,也可能就一个)。
当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
对一个final变量来说,不管它是类Field、实例Field,还是局部变量,只要该变量满足3个条件,这个final变量就不再是一个变量,而是相当于一个直接量。
final String s1 = "aaa"; // 可执行宏替换
final String s2 = s1 // 不可执行宏替换
final String s3 = getValue() // 不可执行宏替换
枚举类,如果不包含抽象方法,那么会默认采用final修饰类,而如果包含抽象方法,那么由于抽象类是不能被final修饰(抽象类如果不实现就没什么意义,用final修饰的话会导致其不能被实现),此时类是被abstract修饰类,所以是可以派生子类的。
使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据flush到物理节点里(因为在执行close()方法中自动执行flush()方法)。Java的很多输出流默认都提供了缓冲功能,其实我们没有必要刻意去记忆哪些流有缓冲功能、哪些流没有,只要正常关闭所有的输出流即可保证程序正常。
@Test
public void testMethod3() {// 实例化老的业务对象(目标类对象)UserDao targetObject = new UserDaoImpl();// 创建InvocationHandler对象InvocationHandler handler = new TransactionHandler(targetObject);/*** jdk类库中有一个类Proxy,其中有一个静态方法newProxyInstance,此方法反回一个代理对象* 参数一:类加载器,为了定位类路径* 参数二:目标对象的所有接口数组* 参数三:是一个类的对象,此类必须实现自InvocationHandler接口,在InvocationHandler接口的接口方法中耦合老业务和新业务功能*/// 创建Proxy代理对象Object proxyObject = Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(), handler);// 代理对象强制转换成接口类型UserDao UserDao = (UserDao)proxyObject;// 用代理对象调用目标方法,用代理对象调用目标方法事实上执行的是InvocationHandler接口方法UserDao.addUser(new User());
}
接口和抽象类都遵循面向接口而不是实现编码设计原则,它可以增加代码的灵活性,可以适应不断变化的需求。下面有几个点可以帮助你回答这个问题:在 Java 中,你只能继承一个类,但可以实现多个接口。所以一旦你继承了一个类,你就失去了继承其他类的机会了。
接口通常被用来表示附属描述或行为如: Runnable 、 Clonable 、 Serializable 等等,因此当你使用抽象类来表示行为时,你的类就不能同时是 Runnable 和 Clonable( 注:这里的意思是指如果把 Runnable 等实现为抽象类的情况 ) ,因为在 Java 中你不能继承两个类,但当你使用接口时,你的类就可以同时拥有多个不同的行为。
在一些对时间要求比较高的应用中,倾向于使用抽象类,它会比接口稍快一点。如果希望把一系列行为都规范在类继承层次内,并且可以更好地在同一个地方进行编码,那么抽象类是一个更好的选择。有时,接口和抽象类可以一起使用,接口中定义函数,而在抽象类中定义默认的实现。