字节面试杂谈——JAVA基础

目录

一、JAVA基本数据类型及其包装类型

二、泛型

三、面向对象的三大特性

四、面向对象与面向过程的区别

五、JDK、JRE、JVM的关系

六、重载和重写

七、构造方法

八、JAVA中创建对象的方式

九、抽象类和接口

十、Object类的常用方法

十一、final、finally、finalize

十二、== 与 equals

十三、hashCode()  与 equals()

十四、实现对象的克隆,深拷贝和浅拷贝

十五、JAVA序列化

十六、反射

十七、动态代理

十八、字节与字符、String为什么不可变。

十九、String,StringBuilder,StringBuffer

二十、异常:Error与Exception、运行时异常与受检异常、throw与throws

二十一、主线程可以捕获到子线程的异常吗

二十二、JAVA中IO流的分类、常用的实现类、字节流和字符流、获取键盘输入

二十三、BIO、NIO、AIO

二十四、成员变量与局部变量

二十五、静态方法和实例方法

二十六、JAVA中值传递

二十七、JAVA中线程的基本状态

二十八、final关键字、static关键字

二十九、Throwable类常用方法

三十、try-catch-finally

三十一、匿名函数 Lambda 表达式

三十二、获得Class对象的方法

三十三、java语言的特点

三十四、杂项



一、JAVA基本数据类型及其包装类型

基本数据类型 所占位数 默认值 对应包装类型

boolean

1 false Boolean
byte 8 0 Byte
char 16 '\u0000' Character
short 16 0 Short
int 32 0 Integer
long 64 0L Long
float 32 0.0f Float
double 64 0.0d Double

注意事项:

(1)变量必须先声明再赋值才能访问。

        成员变量没有手动赋值系统会默认赋值,局部变量不会。

(2)

        小容量可以自动转换成大容量,成为自动类型转换

        大容量不能直接赋值给小容量,需要用强制类型转换符强制转换-->直接砍掉高位,取低位

(3)字面量:

        整数型字面值默认为int类型。

        字面量为 long 类型  -->  123L,123l

        字面量为二进制,八进制,十六进制 --> 0b或者0B,0,0x或者0X

        当一个整数字面值没有超出byte,short,char的取值范围,这个字面值可以直接赋值给byte,short,char类型变量

        浮点型字面值默认是double类型,float x = 1.23 编译错误 --> float x = 1.23f 或者 float x = 1.23F

        boolean类型只能是 true 或者 false

(4)八种基本数据类型当中除 boolean 之外剩下的 7 种类型之间都可以相互转换。

        小容量向大容量转换,称为自动类型转换 --> 整型一定小于浮点型,char 与 short 平级

        byte < short = char < int < long < float < double

        大容量转换成小容量,强制类型转换 --> 需加强制类型转换符

        byte,short,char 参与运算的时候,各自先转换成 int 类型再做运算

        多种数据类型混合运算,先转换成容量最大的那种类型再做运算。        

(5)包装类构造方式

        ① 通过直接赋值 Integer x=1; --> 自动装箱

        自动装箱 Integer.valueOf()  ;   自动拆箱 x.intValue()

        ② 构造器 Integer x=new Integer(1);

        ③ 静态工厂valueOf 方法 Integer x=Integer.valueOf(1);

        new Integer(1) 会新建一个对象并返回引用

        Integer.valueOf(1)会使用整型常量池,范围默认为 [ -128 , 127 ]

(6) int x = Integer.parseInt("123")与int x = new Integer("123")区别

        两个方式都是将字符串类型转变成int类型。第一个直接将字符串转变成int类型。第二个通过调用构造函数方式,先将字符串构建成Integer对象,再通过自动拆包转变成int类型。

(7)

        Boolean    :使用静态final定义,就会返回静态值;

        Byte          :缓存区 -128~127,全部缓存;

        Short         :缓存区 -128~127,部分缓存;

        Character  :缓存区0~127,部分缓存;

        Long          :缓存区-128~127,部分缓存;

        Integer       :缓存区-128~127,部分缓存

        Float 和Double不会有缓存

        Integer是唯一可以修改缓存范围的包装类。只能修改 high 且 high 小于127时不生效。

(8)switch

        Only convertible int values, strings or enum variables are permitted

        其中 byte,short,char 可以自动转换为 int,所以可以使用。

(9)

        当 Integer 和 int 进行比较时,Integer 会自动拆箱为 int。因此就相当于两个 int 比较。

        当两个 Integer 相算术运算时,会先拆箱为 int,运算结果为 int 数据类型

(10)

Set set = new HashSet<>();
for(short i = 0;i<5;i++){
    set.add(i);
    set.remove(i-1);
}
System.out.println(set.size());

        答案:5

        解析:short类型-1之后就变成了int类型,remove()的时候在集合中找不到int类型的数据,所以就没有删除任何元素。

(11)

short s=2;    s=s+1;    编译错误, s+1 操作后,数据类型变为 int 类型,int 类型的数据不能直接赋值给 short 类型

short s=2;    s+=1;      正确,s+=1 等价于 s = (short)(s+1)

(12)

        Integer 和 int 的区别?
                (1)int Java 的八种基本数据类型之一,而  Integer Java int 类型提供的封装类;
                (2) int 型变量的默认值是 0 Integer 变量的默认值是 null ,这⼀点说明 Integer 可以区分出未赋值和值为 0 的 区分;
                (3) Integer 变量必须实例化后才可以使用,而  int 不需要。
        Integer 和 int 的比较延伸
                1、由于 Integer 变量实际上是对⼀个 Integer 对象的引用,所以两个通过 new 生 成的 Integer 变量永远是不相等的,因为其内存地址是不同的;
                2、Integer 变量和 int 变量比较时,只要两个变量的值是相等的,则结果为 true 。因为包装类 Integer 和基本数据类型 int 类型进行比较时, Java 会自动拆包装类为 int ,然后进行比较,实际上就是两个 int 型变量在进行比较;
                3、非  new 生 成的 Integer 变量和 new Integer() 生 成的变量进行比较时,结果为 false 。因为非  new 生成的Integer 变量指向的是 Java 常量池中的对象,而  new Integer() 生成的变量指向堆中新建的对象,两者在内存中的 地址不同;
                4、对于两个非 new ⽣成的 Integer 对象进行比较时,如果两个变量的值在区间 [-128,127] 之间,则比较结果为 true ,否则为 false Java 在编译 Integer i = 100 时,会编译成 Integer i = Integer.valueOf(100) ,而  Integer 类型的 valueOf 的源码如下所示
        
public static Integer valueOf(int var0) {

    return var0 >= -128 && var0 <= Integer.IntegerCache.high ?
                            Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);

}
                从上面的代码中可以看出:Java 对于 [-128, 127] 之间的数会进行缓存,比如: Integer i = 127 ,会将 127 进行缓存,下次再写 Integer j = 127 的时候,就会直接从缓存中取出,而对于这个区间之外的数就需要 new 了。
        包装类的缓存:
                Boolean:全部缓存
                Byte:全部缓存
                Character:<= 127 缓存
                Short:-128 — 127 缓存
                Long:-128 — 127 缓存
                Integer:-128 — 127 缓存
                Float:没有缓存
                Doulbe:没有缓存

(13)

        装箱和拆箱
                自动装箱是 Java 编译器在基本数据类型和对应得包装类之间做的⼀个转化。比如:把 int 转化成 Integer ,double 转化成 Double 等等。反之就是自动拆箱。
                原始类型:boolean、 char byte short int long float double
                封装类型:Boolean、 Character Byte Short Integer Long Float Double
(14)
        switch 语句能否作用在 byte 上,能否作用在 long 上,能否作用在 String 上?
                在 switch(expr 1) 中, expr1 只能是⼀个整数表达式或者枚举常量。而整数表达式可以是 int 基本数据类型或者Integer 包装类型。由于, byte short char 都可以隐式转换为 int ,所以,这些类型以及这些类型的包装类型也 都是可以的。
                而 long 和 String 类型都不符合 switch 的语法规定,并且不能被隐式的转换为 int 类型,所以,它们 不能作⽤于 switch 语句中。不过,需要注意的是在 JDK1.7 版本之后 switch 就可以作⽤在 String 上了。

二、泛型

(1)泛型类的定义与使用

        修饰符 class 类名<代表泛型的变量> {  }

//定义
public class Generic  {
	private T ans;
	
	public Generic(T ans){
		this.ans = ans;
	}
	
	public void setAns(T ans) {
		this.ans = ans;
	}
	
	public T getAns() {
		return ans;
	}
}


//使用
import java.util.ArrayList;
import java.util.List;

public class Main {
	public static void main(String[] args) {
		
		Generic gen1 = new Generic<>("abc");
		System.out.println(gen1.getAns());
		
		Generic> gen2 = new Generic<>(new ArrayList<>());
		gen2.getAns().add(1);
		gen2.getAns().add(2);
		gen2.getAns().add(3);
		System.out.println(gen2.getAns().size());
		
	}
}

        类型变量使用大写形式,且比较短, 这是很常见的。在 Java 库中, 使用变量 E 表示集合的元素类型, K 和 V 分别表示表的关键字与值的类型。T ( 需要时还可以用临近的字母 U 和 S) 表示“ 任意类型”。

(2)泛型方法的定义与使用

        修饰符 <代表泛型的变量> 返回值类型 方法名(参数){  }

class Array{
	public static  T getArrayMid(T...ts) {
		return ts[ts.length/2];
	}
}

        这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是 public static) 的后面,返回类型的前面。
        泛型方法可以定义在普通类中,也可以定义在泛型类中。

        当调用一个泛型方法时在方法名前的尖括号中放人具体的类型:String ans = Array.getArrayMid("1","2","3","4","5");
        在这种情况(实际也是大多数情况)下,方法调用中可以省略 类型参数。编译器有足够的信息能够推断出所调用的方法。它用 names 的类型(即 String[ ]) 与泛型类型 T[ ]进行匹配并推断出 T 一定是 String。也就是说,可以调用String ans2 = Array.getArrayMid("1","2","3","4","5");

public class Main {
	public static void main(String[] args) {
		
		String ans = Array.getArrayMid("1","2","3","4","5");
		System.out.println(ans);
		
		Integer num = Array.getArrayMid(1,2,3,4,5,6,7,8);
		System.out.println(num);
		
		String ans2 = Array.getArrayMid("1","2","3","4","5");
		System.out.println(ans2);
		
		Integer num2 = Array.getArrayMid(1,2,3,4,5,6,7,8);
		System.out.println(num2);
		
	}
}

(3)类型变量的限定

        有时,类或方法需要对类型变量加以约束。下面是一个典型的例子。我们要计算数组中的最小元素:

class ArrayAIg
{
    public static  T min(T[] a) // almost correct
    {
        if (a null || a.length = 0) return null ; 
        T smallest = a[0];
        for (int i = 1; i < a.length; i ++)
            if (smallest.compareTo(a[i]) > 0) smallest = a[i];
        return smallest; 
    } 
}

        但是,这里有一个问题。请看一下 min方法的代码内部。 变量 smallest 类型为 T, 这意味着它可以是任何一个类的对象。怎么才能确信 T 所属的类有 compareTo 方法呢?
        解决这个问题的方案是将 T 限制为实现了 Comparable 接口(只含一个方法 compareTo 的
标准接口)的类。可以通过对类型变量 T 设置限定(bound) 实现这一点:

class ArrayAlg{
	public static T min(T[] a) {
		if(a==null || a.length==0) {
			return null;
		}
		T min = a[0];
		for(int i = 1; i < a.length; i++) {
			if(min.compareTo(a[i]) > 0) min = a[i];
		}
		return min;
	}
}

        实际上 Comparable 接口本身就是一个泛型类型。目前, 我们忽略其复杂性以及编译器产生的警告。现在,泛型的 min方法只能被实现了 Comparable 接口的类(如 String、 LocalDate 等)的数组调用。

        在此为什么使用关键字 extends 而不是 implements ? 毕竟,Comparable 是一个接口。下面的记法表示 T 应该是绑定类型的子类型 (subtype)。 T 和绑定类型可以是类, 也可以是接口。选择关键字 extends 的原因是更接近子类的概念, 并且 Java 的设计者也不打算在语言中再添加一个新的关键字(如 sub)。
        一个类型变量或通配符可以有多个限定, 例如: T extends Comparable & Serializable限定类型用“ &” 分隔,而逗号用来分隔类型变量。

        在 Java 的继承中, 可以根据需要拥有多个接口超类型, 但限定中至多有一个类如果用
一个类作为限定,它必须是限定列表中的第一个。

(4)泛型擦除

        1.虚拟机中没有泛型,只有普通的类和方法。

                无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型 ( raw type )。原始类型的名字就是删去类型参数后的泛型类型名。擦除( erased) 类型变M, 并替换为限定类型(无限定的变量用 Object)。结果是一个普通的类, 就好像泛型引人 Java 语言之前已经实现的那样。原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换。

public class Interval 

        2.所有的类型参数都用它们的限定类型替换。

        当程序调用泛型方法时,如果擦除返回类型, 编译器插入强制类型转换。例如,下面这个语句序列
                Pair buddies = . .
                Employee buddy = buddies.getFirst();
擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插人 Employee 的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

                •对原始方法 Pair.getFirst 的调用。

                •将返回的 Object 类型强制转换为 Employee 类型。


        当存取一个泛型域时也要插人强制类型转换。假设 Pair 类的 first 域和 second 域都是公有的(也许这不是一种好的编程风格,但在 Java 中是合法的)。表达式:Employee buddy = buddies.first;  也会在结果字节码中插人强制类型转换。

        3.桥方法被合成来保持多态。

        set

        Datelnterval 重写Pair的set方法并指定参数类型

        编译器在 Datelnterval 类中生成一个桥方法(bridge method):
        public void setSecond(Object second) { setSecond((Date) second); }     

        get

        具有相同参数类型的两个方法是不合法的。两个类的get方法它们都没有参数。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此, 编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。

        桥方法不仅用于泛型类型。 在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。例如:
                public class Employee implements Cloneable
                {
                        public Employee clone() throws CloneNotSupportedException { . . . }

                }
        Object.clone 和 Employee.clone 方法被说成具有协变的返回类型 (covariant return types)。
        实际上,Employee 类有两个克隆方法:
                Employee clone() // defined above
                Object clone() // synthesized bridge method, overrides Object,clone
        合成的桥方法调用了新定义的方法

        4.为保持类型安全性,必要时插人强制类型转换

(5)约束与局限性

        1.不能用基本类型实例化类型参数

        2.运行时类型查询只适用于原始类型

                虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
                例如:
                        if (a instanceof Pair) // Error
                实际上仅仅测试 a 是否是任意类型的一个 Pair。下面的测试同样如此:
                        if (a instanceof Pair) // Error
                或强制类型转换:
                        Pair p = (Pair) a; // Warning-can only test that a is a Pair
                为提醒这一风险, 试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会得到一个编译器错误, 如果使用强制类型转换会得到一个警告。
                同样的道理, getClass 方法总是返回原始类型。例如:
                        Pair stringPair = . .
                        PairemployeePair = . .
                        if (stringPair.getClass() == employeePair.getClass()) // they are equal
                其比较的结果是 true, 这是因为两次调用 getClass 都将返回 Pair.class。

        3.不能创建参数化类型的数组

                不能实例化参数化类型的数组, 例如:
                        Pair[] table = new Pair[10]; // Error
                这有什么问题呢? 擦除之后, table 的类型是 Pair[]。可以把它转换为 Object[] :
                        Object[] objarray = table;
                数组会记住它的元素类型, 如果试图存储其他类型的元素, 就会抛出一个ArrayStoreException 异常:
                        objarray[0] = "Hello"; // Error component type is Pair
                不过对于泛型类型, 擦除会使这种机制无效。以下赋值:
                        objarray[0] = new Pair();
                能够通过数组存储检査, 不过仍会导致一个类型错误。出于这个原因, 不允许创建参数化类型的数组。需要说明的是, 只是不允许创建这些数组, 而声明类型为 Pair[] 的变量仍是合法的。不过不能用 new Pair[10] 初始化这个变量。

                可以声明通配类型的数组, 然后进行类型转换:Pair[] table = (Pair[]) new Pair[10];结果将是不安全的。如果在 table[0] 中存储一个 Pair<Employee>, 然后对 table[0].getFirst() 调用一个 String 方法会得到一个 ClassCastException 异常

                如果需要收集参数化类型对象, 只有一种安全而有效的方法 :使用 ArrayList : ArrayList< Pair < String>>
        4.Varargs 警告

                向参数个数可变的方法传递一个泛型类型的实例。
                考虑下面这个简单的方法, 它的参数个数是可变的:
                        public static void addAll(Collections coll, T... ts) {
                                for (t : ts) coll.add(t)
                          }
                应该记得,实际上参数 ts 是一个数组, 包含提供的所有实参。
                现在考虑以下调用:
                        Col1ection> table = . . .;
                        Pair pairl = . . .;
                        Pair pair2 = . .
                        addAll(table, pairl, pair2);
                为了调用这个方法,Java 虚拟机必须建立一个 Pair 数组, 这就违反了前面的规则。不过,对于这种情况, 规则有所放松,你只会得到一个警告,而不是错误。
                可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解 @SuppressWamings("unchecked")。或者在 Java SE 7中, 还 可 以 用@SafeVarargs 直 接 标 注addAll 方法:@SafeVarargs public static void addAll(Collection coll, T... ts)

                现在就可以提供泛型类型来调用这个方法了。对于只需要读取参数数组元素的所有方法,都可以使用这个注解,这仅限于最常见的用例。

                可以使用 @SafeVarargs 标注来消除创建泛型数组的有关限制, 方法如下:
                        @SafeVarargs static EQ array(E... array) { return array; }
                现在可以调用:
                        Pair[] table = array(pairl,pair2);
                这看起来彳艮方便,不过隐藏着危险。以下代码:
                        Object[] objarray = table;
                        objarray[0] = new Pair();
                能顺利运行而不会出现 ArrayStoreException 异常(因为数组存储只会检查擦除的类型 ),但在处理 table[0] 时你会在别处得到一个异常。

        5.不能实例化类型变量

                不能使用像 new T(...),newT[...] 或 T.class 这样的表达式中的类型变量。例如, 下面的Pair 构造器就是非法的:
                        public Pair() { first = new T(); second = new T(); } // Error
                类型擦除将 T 改变成 Object, 而且, 本意肯定不希望调用 new Object()。在 Java SE 8 之后,最好的解决办法是让调用者提供一个构造器表达式。例如:
                        Pair p = Pair.makePair(String::new);
                makePair 方法接收一个 Supplier,这是一个函数式接口,表示一个无参数而且返回类型为 T 的函数:
                        public static Pair makePair(Supplier constr) {
                                return new Pair<>(constr.get(), constr.get());
                          }


                比较传统的解决方法是通过反射调用 Clasmewlnstance 方法来构造泛型对象。遗憾的是,细节有点复杂。不能调用:
                        first = T.dass.newInstanceO; // Error
                表达式 T.class 是不合法的, 因为它会擦除为 Objectclass。必须像下面这样设计 API 以便得到一个 Class 对象:
                        public static Pair makePair(Class cl) {
                                try {
                                           return new Pair(d.newInstance(), cl.newInstance());
                                }catch (Exception ex) { return null; }
                        }
                这个方法可以按照下列方式调用
                        Pair p = Pair.makePair(String.class);
                注意,Class类本身是泛型。 例如,String.class 是一个 Class 的实例(事实上,它是唯一的实例)。因此,makePair 方法能够推断出 pair 的类型。

        6.不能构造泛型数组
                就像不能实例化一个泛型实例一样, 也不能实例化数组。不过原因有所不同,毕竟数组会填充 null 值,构造时看上去是安全的。不过, 数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。 例如,考虑下面的例子:
                public static T[] minmax(T[] a) { T[] mm = new T[2]; . . . } // Error
                类型擦除会让这个方法永远构造 Comparable[2] 数组。如果数组仅仅作为一个类的私有实例域, 就可以将这个数组声明为 Object[],并且在获取元素时进行类型转换。

                让用户提供一个数组构造器表达式:
                        String[] ss = ArrayAlg.minmax (String[]::new,"Tom", "Dick", "Harry");
                构造器表达式 String[]::new 指示一个函数, 给定所需的长度, 会构造一个指定长度的String 数组。minmax 方法使用这个参数生成一个有正确类型的数组:
                        public static T[] minmax(IntFunction constr, T... a) {
                                T[] mm = constr.apply(2);
                        }


                比较老式的方法是利用反射, 调用 Array.newlnstance:
                        public static T[] minmax(T... a) {
                                T[] mm = (T[]) Array.newlnstance (a.getClass().getComponentType() , 2);
                        }


                ArrayList 类的 toArray 方法就没有这么幸运。它需要生成一个 T[] 数组, 但没有成分类型。因此, 有下面两种不同的形式:
                Object[] toArray()
                T[] toArray(T[] result)
第二个方法接收一个数组参数。如果数组足够大, 就使用这个数组。 否则, 用 result 的成分类型构造一个足够大的新数组。

                还可以:
                         java.lang.reflect.Array.newInstance(Class componentType, int length)方法来创建一个具有指定类型和维度的数组。

        7.泛型类的静态上下文中类型变量无效

                不能在静态域或方法中引用类型变量。例如, 下列高招将无法施展:
                        public class Singleton {
                                private static T singlelnstance; // Error
                                public static T getSinglelnstance() // Error
                                {
                                        if (singleinstance == null) construct new instance of T
                                        return singlelnstance;
                                }
                         }
                如果这个程序能够运行, 就可以声明一个 Singleton 共享随机数生成器, 声明一个 Singlet0n 共享文件选择器对话框。但是, 这个程序无法工作。类型擦除之后, 只剩下 Singleton 类,它只包含一个 singlelnstance 域。 因此, 禁止使用带有类型变量的静态域和方法。

        8.不能抛出或捕获泛型类的实例

                既不能抛出也不能捕获泛型类对象

                catch 子句中不能使用类型变量。例如, 以下方法将不能编译:
                        public static void doWork(Class t)
                        {
                                try
                                {
                                        do work
                                }
                                catch (T e) // Error can 't catch type variable
                                {
                                        Logger,global.info(...)
                                }
                        }
                不过, 在异常规范中使用类型变量是允许的。以下方法是合法的:
                        public static void doWork(T t) throws T
                        { // OK
                                try
                                {
                                        do work
                                }
                                catch (Throwable real Cause)
                                {
                                        t.initCause(real Cause);
                                        throw t;
                                }
                        }

        9.可以消除对受查异常的检查

                Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器 。不过可以利用泛型消除这个限制。关键在于以下方法
                        @SuppressWamings(" unchecked ")
                        public static extends Throwable>  void throwAs ( Throwable e ) throws T
                        {
                                throw CO e;
                        }
                假设这个方法包含在类 Block 中 如果调用 Block. > throwAs ( t ); 编译器就会认为 t 是一个非受查异常

        

        10.注意擦除后的冲突

                 泛型规范说明还提到另外一个原则:

                   “ 要想支持擦除的转换, 就需要强行限制一个类或类 型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。” 例如,
                下述代码是非法的:
                        class Employee implements Coinparab1e< Emp 1 oyee > { . . . }
                        class Manager extends Employee implements Comparable
                        {
                .
                        . . } // Error
                Manager 会实现 Comparable< Employee > Comparable < Manager > , 这是同一接口的不同 参数化。
                这一限制与类型擦除的关系并不十分明确。毕竟, 下列非泛型版本是合法的。
                        class Employee implements Comparable { . . . }
                        class Manager extends Employee implements Comparable { . . . }
                其原因非常微妙, 有可能与合成的桥方法产生冲突。 实现了 Co mpamble < X > 的类可以获得一 个桥方法: public int compareTo ( Object other ) { return compareTo ( ( X ) other ) ; } 对于不同类型的 X 不能有两个这样的方法。  

    

(6)泛型类型的继承规则
        考虑一个类和一个子类, 如 Employee Manager Pair < Manager > Pair > 的一个子类吗 答案是 不是”。
        无论 S 与 T 有什么联系 ,通常 Pair > Pair < T >没 有什么联系。
        必须注意泛型与 Java 数组之间的重要区别 可以将一个 Manager[]  数组赋值给一个类型为Employee[ ] 的变量:数组带有特别的保护。如果试图将一个低级别的雇员存储到employeeBuddies[0],虚拟机将会抛出 ArrayStoreException 异常。        

       

     
(7)通配符
        通配符类型中, 允许类型参数变化
       
        ①Pair<? extends Employee>表示任何泛型 Pair 类型 它的类型参数是 Employee 及其 子类。
                Pair extends Employee>。其方法似乎是这样的
                        ? extends Employee getFi rst()
                        void setFirst(? extends Employee )
                这样将不可能调用 setFirst 方法 编译器只知道需要某个 Employee 的子类型 但不知道 具体是什么类型。 它拒绝传递任何特定的类型 毕竟 不能用来匹配
                使用 getFirst 就不存在这个问题 getFirst 的返回值赋给一个 Employee 的引用完全合法。  

       

        ②? super Manager这个通配符限制为 Manager 的所有超类型
                可以为方法 提供参数, 但不能使用返回值。 例如 Pair < ? super Manager > 有方法
                        void setFirst(? super Manager )
                        ? super Manager getFirst ( )
                这不是真正的 Java 语法, 但是可以看出编译器知道什么 编译器无法知道 setFirst 方法 的具体类型, 因此调用这个方法时不能接受类型为 Employee Object 的参数 只能传递 Manager 类型的对象 或者某个子类型 Executive ) 对象 另外 如果调用 getFirst , 不能 保证返回对象的类型 只能把它赋给一个 Object
        直观地讲,带有超类型限定的通配符可以向泛型对象写人,带有子类型限定的通配符可 以从泛型对象读取
                public static < T extends Comparable < T>>   T mi n(T[]  a )
                public static < T extends Comparable < ? super T>>   T m in(T [ ] a) 现在 compareTo 方法写成 int compareTo ( ? super T)  

       

        ③Pair
                还可以使用无限定的通配符, 例如, Pair < ? > 初看起来 这好像与原始的 Pair 类型一样。
                实际上, 有很大的不同。类型 Pair < ? > 有以下方法:
                        ? getFi rst()
                        void setFirst(?)
                getFirst 的返回值只能赋给一个 Object。 setFirst 方法不能被调用 甚至不能用 Object 调 用。Pair < ? > Pair 本质的不同在于 可以用任意 Object 对象调用原始 Pair 类的 setObject
方法。   

       

(8)泛型Class类
字节面试杂谈——JAVA基础_第1张图片
    

       

(9)使用 Class<T> 参数进行类型匹配
字节面试杂谈——JAVA基础_第2张图片

(10)类型擦除有什么优势
        减小运行时内存负担
        向前兼容性好
(11)类型擦除存在什么问题
a.基本类型无法作为泛型实参
List intArray;          // compile error
List intArray;      // compile success
List intArray;       // compile error
List intArray;       // compile success
b. 泛型类型无法用作方法重载
public void testMethod(List array) {}
public void testMethod(List array) {}    // compile error

c.泛型类型无法当做真实类型使用

static  void genericMethod(T t) {
  T newInstance = new T();              // compile errror
  Class c = T.class;                    // compile errror
  List list = new ArrayList();    // compile errror
  if (list instance List) {}   // compile errror
}

d.静态方法无法引用类泛型参数

class GenericClass {
  public static T max(T a, T b) {}
}

e.泛型类型会带来类型强转的运行时开销

List strList = new Array<>();
strList.add("Hallo");
String value = strList.get(0);

但实际字节码指令执行strList.get()方法时,经过类型擦除后,还是需要做类型强转:

INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object;
CHECKCAST java/lang/String

(12)类型擦除后怎么获取泛型参数?

        在Java中,泛型类型虽然被擦除了,但是被擦除的类型信息还是会以某种形式存储下来,并支持在运行时获取。这种形式就是指元素附加的签名信息( Signatures
class GenericClass {}
class ConcreteClass extends GenericClass {
  public List getArray() {}
}

// 获取类元素泛型
ParameterizedType genericType = 
       (ParameterizedType)ConcreteClass.class.getGenericSuperClass();
// 获取方法元素泛型
ParameterizedType genericType = 
       (ParameterizedType)ConcreteClass.class.getMethod("getArray").getGenericReturnTypes();
  • 获取属性上的泛型类型:
    • field.getGenericType();
  • 获取方法结构——形参的泛型类型:
    • method.getGenericParameterTypes();
  • 获取方法结构——返回值的泛型类型:
    • method.getGenericReturnType();

三、面向对象的三大特性

        (1 )封装:通常认为封装是把数据和操作数据的方法封装起来,对数据的访问只能通过已定义的接口。
        (2)继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类 /基类),得到继承信息的被称为子类(派生类)。
        (3)多态:分为编译时多态(方法重载)和运行时多态(方法重写)。要实现多态需要做两件事:⼀是子类继承父类并重写父类中的方法,⼆是用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。
几点补充
        (1)子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。因为在⼀个子类被创建的时候,首先会在内存中创建⼀个父类对象,然后在父类对象外部放上子类独有的属性,两者合起来形成⼀个子类的对象;
        (2 )子类可以拥有自己属性和方法;
        (3)子类可以用自己的方式实现父类的方法。(重写)
        1. 封装
                封装把⼀个对象的属性私有化,同时提供⼀些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果⼀个类没有提供给外界访问的方法,那么 这个类也没有什么意义了。
        2. 继承
                继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
        关于继承如下 3 点请记住:
                1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问, 只是拥有
                2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
                3. 子类可以用自己的方式实现父类的方法。(方法重写)。
        3. 多态
                所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即⼀个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
                在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并 覆盖接口中同一方法)。

四、面向对象与面向过程的区别

        面向对象是⼀种基于面向过程的编程思想,是向现实世界模型的自然延伸,这是⼀种“ 万物皆对象”的编程思想。由执行者变为指挥者,在现实⽣活中的任何物体都可以归为⼀类事物,而每⼀个个体都是⼀类事物的实例。面向对象的编程是以对象为中心,以消息为驱动。
区别 :
        (1)编程思路不同:面向过程以实现功能的函数开发为主,而面向对象要首先抽象出类、属性及其方法,然后通过实例化类、执行方法来完成功能。
        (2 )封装性:都具有封装性,但是面向过程是封装的是功能,而面向对象封装的是数据和功能。
        (3 )面向对象具有继承性和多态性,而面向过程没有继承性和多态性,所以面向对象优势很明显

面向过程 面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、 Linux/Unix 等。⼀般采用面向过程开发。但是, ⾯向过程没有面向对象易维护、易复用、易扩展。
面向对象 面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是, 面向对象性能比面 向过程低
        这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语⾔,而是 Java 是半编译语⾔,最终的执行代码并不是可以直接被 CPU 执行的⼆进制机械码。
        而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它⼀些面向过程的脚本语⾔性能也并不⼀定比 Java 好。

五、JDK、JRE、JVM的关系

        JDK( Java Development Kit ):是 Java 开发⼯具包,是整个 Java 的核心,包括了 Java 运行环境 JRE Java ⼯具 和 Java 基础类库。
        JRE( Java Runtime Environment ):是 Java 的运行环境,包含 JVM 标准实现及 Java 核心类库。
        JVM( Java Virtual Machine ):是 Java 虚拟机,是整个 Java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。所有的 Java 程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。
        1. JVM
                Java 虚拟机(JVM )是运行  Java 字节码的虚拟机。 JVM 有针对不同系统的特定实现( Windows Linux macOS ),目的是使用相同的字节码,它们都会给出相同的结果。
        什么是字节码? 采⽤字节码的好处是什么 ?
                在 Java 中, JVM 可以理解的代码就叫做 (即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。 Java 语言通过字节码的方式,在⼀定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对⼀种特定的机器,因此, Java 程序无须重 新编译便可在多种不同操作系统的计算机上运行。
        Java 程序从源代码到运行⼀般有下面  3 步:
字节面试杂谈——JAVA基础_第3张图片

        我们需要格外注意的是 .class-> 机器码 这⼀步。在这⼀步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的 ( 也就是所谓的热点代码 ) ,所以后面引进了 JIT 编译器, 而JIT 属于运行时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语⾔
        HotSpot 采用了惰性评估 (Lazy Evaluation) 的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出⼀些优化,因此执行的次数越多,它的速度就越快。 JDK 9 引入了⼀种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。 JDK 支持 分层编译和 AOT 协作使用。但是 , AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
        Java 虚拟机( JVM )是运行  Java 字节码的虚拟机。 JVM 有针对不同系统的特定实现Windows, Linux macOS ),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语⾔ ⼀次编译,随处可以运行 的关键所在。
        2. JDK 和 JRE
                JDK 是 Java Development Kit ,它是功能齐全的 Java SDK 。它拥有 JRE 所拥有的⼀切,还有编译器( javac )和工具(如 javadoc jdb )。它能够创建和编译程序。
                JRE 是 Java 运行 时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机 ( JVM ), Java 类库, java 命令和其他的⼀些基础构件。但是,它不能用于创建新程序。
                如果你只是为了运行⼀下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行⼀些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打 算在计算机上进⾏任何 Java 开发,仍然需要安装 JDK 。例如,如果要使用  JSP 部署 Web 应用程序,那么从技术上讲,您只是在应⽤程序服务器中运行  Java 程序。那你为什么需要 JDK呢? 因为应用程序服务器会将 JSP 转换为 Java servlet ,并且需要使用  JDK 来编译 servlet
        3. Oracle JDK 和 OpenJDK 的对⽐
        可能在看这个问题之前很多⼈和我⼀样并没有接触和使用过 OpenJDK 。那么 Oracle
OpenJDK 之间是否存在重⼤差异?下⾯我通过收集到的⼀些资料,为你解答这个被很多⼈忽视
的问题。
        对于 Java 7 ,没什么关键的地方。 OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外, OpenJDK 被选为 Java 7 的参考实现,由 Oracle ⼯程师维护。关于 JVM JDK JRE 和OpenJDK 之间的区别, Oracle 博客帖⼦在 2012 年有⼀个更详细的答案
        问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别?
                答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部 分,例如部署代码,其中包括 Oracle Java 插件和 Java WebStart 的实现,以及⼀些封闭的源代码派对组件,如图形光栅化器,⼀些开源的第三⽅组件,如 Rhino,以及⼀些零碎的东西,如附加⽂档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部 分,除了我们考虑商业功能的部分。
        1. Oracle JDK 大概每 6 个月发⼀次主要版本,而  OpenJDK 版本⼤概每三个月发布⼀次。但这不是固定的,我觉得了解这个没啥用处。详情参见: https://blogs.oracle.com/java-platfor
m-group/update-and-faq-on-the-java-se-release-cadence
        2. OpenJDK 是⼀个参考模型并且是完全开源的,⽽ Oracle JDK OpenJDK 的⼀个实现,并 不是完全开源的;
        3. Oracle JDK 比  OpenJDK 更稳定。 OpenJDK Oracle JDK 的代码几乎相同,但 Oracle
JDK 有更多的类和⼀些错误修复。因此,如果您想开发企业 /商业软件,我建议您选择 Oracle JDK ,因为它经过了彻底的测试和稳定。某些情况下,有些⼈提到在使用 OpenJDK 可能会遇到了许多应⽤程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题;
        4. 在响应性和 JVM 性能方面, Oracle JDK OpenJDK 相比 提供了更好的性能;
        5. Oracle JDK 不会为即将发布的版本提供⻓期⽀持,⽤户每次都必须通过更新到最新版本获
得⽀持来获取最新版本;
        6. Oracle JDK 根据⼆进制代码许可协议获得许可,而  OpenJDK 根据 GPL v2 许可获得许可。
        4. Java 和 C++ 的区别 ?
                都是面向对象的语言,都⽀持封装、继承和多态
                Java 不提供指针来直接访问内存,程序内存更加安全
                Java 的类是单继承的,C++ ⽀持多重继承;虽然 Java 的类不可以多继承,但是接⼝可以多 继承。
                Java 有自动内存管理机制,不需要程序员手动释放无用内存
                在 C 语言中,字符串或字符数组最后都会有⼀个额外的字符 ‘\0’ 来表示结束。但是, Java 语言 中没有结束符这⼀概念。

六、重载和重写

        (1 )重载:编译时多态、同⼀个类中同名的方法具有不同的参数列表、不能根据返回类型进行区分【因为:函数调用时不能指定类型信息,编译器不知道你要调哪个函数】;
        (2)重写(⼜名覆盖):运行时多态、子类与父类之间、子类重写父类的方法具有相同的返回类型、更好的访问权限
        重载就是同样的⼀个方法能够根据输⼊数据的不同,做出不同的处理
        重写就是当子类继承自父类的相同方法,输⼊数据⼀样,但要做出有别于父类的响应时,你就要覆盖父类方法
        重载:
                发生在同⼀个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。

        综上:重载就是同⼀个类中多个同名方法根据不同的传参来执行不同的逻辑处理。

        重写:
                重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
                1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
                2. 如果父类⽅法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
                3. 构造方无⽆法被重写
                综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
区别点
重载方法 重写方法
发生范围 同⼀个类 子类
参数列表 必须修改 ⼀定不能修改
返回类型 可修改 子类方法返回值类型应比父类方法返回值类型更小或相等
异常 可修改 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
访问修饰符 可修改 ⼀定不能做更严格的限制(可以降低限制)
发生阶段 编译期 运⾏期
当法的重写要遵循两同两小一大
        “两同 即方法名相同、形参列表相同;
        “两小 指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
        “一大 ”指的是子类方法的访问权限应比父类方法的访问权限更大或相等,如果方法的返回类型是 void和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
Java 中是否可以重写⼀个 private 或者 static 方 法?
        Java 中 static ⽅法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而  static 方 法是编译时静态绑定的。
        static 方法跟类的任何实例都不相关,所以概念上不适用。
        Java 中也不可以覆盖 private 的方法,因为 private 修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到 private 变量或方法的,当然也不能覆盖。

        静态方法补充:静态的方法可以被继承,但是不能重写。如果父类和子类中存在同样名称和参数的静态方法,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。通俗的讲就是父类的方法和子类的方法是两个没有关系的方法,具体调用哪⼀个方法是看是哪个对象的引用;这种父子类方法也不在存在多态的性质。
 

七、构造方法

        构造器是否可以被重写?
                在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以 Constructor也就不能被 Override (重写),但是可以 Overload (重载),所以你可以看到⼀个类中有多个构造函数的情况。
        
        构造方法有哪些特性?
                (1)名字与类名相同;
                (2)没有返回值,但不能用  void 声明构造函数;
                (3)生成类的对象时自动执行,⽆需调用。
                Constructor 不能被 override (重写) , 但是可以 overload (重载) ,所以你可以看到⼀个类中有多个构造函数的情况。
        在 Java 中定义一个不做事且没有参数的构造方法有什么作用?
                Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中 “没有参数的构造方法
                因此,如果父类中只定义了有参数的构造⽅法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是:在父类里加上⼀个不做事且没有参数的构造方法。
        在 Java 中定义⼀个不做事且没有参数的构造方法的作用
                Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中 没有参数的构造方法 ”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用  super() 来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上⼀个不做事且没有参数的构造方法。
        在⼀个静态方法内调用⼀个非静态成员为什么是非法的?
                由于静态方法可以不通过对象进行调用,因此在静态方法⾥,不能调用其他非静态变量,也不可以访问非静态变量成员。

八、JAVA中创建对象的方式

        1、使用  new 关键字;
        2、使用  Class 类的 newInstance 方法,该方法调用无参的构造器创建对象(反射):Class.forName.newInstance()
        3、使用  clone() 方 法;
        4、反序列化,比如调用  ObjectInputStream 类的 readObject() ⽅法。

九、抽象类和接口

        (1)抽象类中可以定义构造函数,接口不能定义构造函数;
        (2)抽象类中可以有抽象方法和具体方法,而接口中只能有抽象方法( public abstract );
        (3 )抽象类中的成员权限可以是 public 、默认、 protected,private(但是抽象方法不可以是private,抽象类中抽像方法就是为了重写,所以不能被 private 修饰),而接口中的成员只可以是 public (方法默认: public abstrat 、成员变量默认:public static final );
        (4 )抽象类中可以包含静态方法,而接口中不可以包含静态方法;
        JDK 8 中的改变
                1、在 JDK1.8 中,允许在接⼝中包含带有具体实现的方法,使用  default 修饰,这类方法就是默认方法。
                2、抽象类中可以包含静态方法,在 JDK1.8 之前接口中不能包含静态方法,JDK1.8 以后可以包含。之前不能包含是因为,接口不可以实现⽅法,只可以定义方法,所以不能使用静态方法(因为静态方法必须实现)。现在可以包含了,只能直接用接口调用静态方法。 JDK1.8 仍然不可以包含静态代码块。
                静态变量:是被 static 修饰的变量,也称为类变量,它属于类,因此不管创建多少个对象,静态变量在内存中有且仅有⼀个拷贝;静态变量可以实现让多个对象共享内存。
                实例变量:属于某⼀实例,需要先创建对象,然后通过对象才能访问到它。
抽象类和接口的区别
 
        1. 接口的方法默认是 public ,所有方法在接口中不能有实现 (Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
        2. 接口中除了 static final 变量,不能有其他变量,而抽象类中则不⼀定。
        3. ⼀个类可以实现多个接口,但只能实现⼀个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
        4. 接口方法默认修饰符是 public ,抽象方法可以有 public protected default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
        5. 从设计层层来说,抽象是对类的抽象,是⼀种模板设计,而接⼝是对行为的抽象,是⼀种行为的规范。
        在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了⼀样的默认方法,则必须重写,不然会报错。
        jdk9 的接口被允许定义私有方法 。
总结⼀下 jdk7~jdk9 Java 中接口概念的变化
        1. 在 jdk 7 或更早版本中,接口里面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。
        2. jdk 8 的时候接口可以有默认方法和静态方法功能。
        3. Jdk 9 在接口中引入了私有方法和私有静态方法。

十、Object类的常用方法

        clone 方法:用于创建并返回当前对象的⼀份拷贝;
        
        getClass 方法:用于返回当前运行时对象的 Class
        toString 方法:返回对象的字符串表示形式;
        
        finalize 方法:实例被垃圾回收器回收时触发的方法;
        equals 方法:用于比较两个对象的内存地址是否相等,⼀般需要重写;
        hashCode 方法:用于返回对象的哈希值;
        notify 方法:唤醒⼀个在此对象监视器上等待的线程。如果有多个线程在等待只会唤醒⼀个。
        notifyAll 方法:作用跟 notify() ⼀样,只不过会唤醒在此对象监视器上等待的所有线程,而不是⼀个线程。
        wait 方法:让当前对象等待;

十一、final、finally、finalize

        final:用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、被其修饰的类不可继承;
        finally:异常处理语句结构的⼀部分,表示总是执行;
        finallize: Object 类的⼀个方法,在垃圾回收时会调用被回收对象的 finalize

十二、== 与 equals

        ==:如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
        equals 方法:用来比较两个对象的内容是否相等。注意: equals 方法不能用于比较基本数据类型的变量。如果没有对 equals 方 法进行重写,则比较的是引用类型的变量所指向的对象的地址(很多类重新写了 equals 方法,比如 String Integer 等把它变成了值比较,所以⼀般情况下 equals 比 较的是值是否相等)。
        == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象 (基本数据 类型 ==比较 的是值,引用数据类型 ==比较 的是内存地址 )
        equals() : 它的作用也是判断两个对象是否相等。但它⼀般有两种使用情况:
                情况 1:类没有覆盖 equals() 方 法。则通过 equals() 比较该类的两个对象时,等价于通“==”比较这 两个对象。
                情况 2:类覆盖了 equals() 方 法。⼀般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true ( 即,认为这两个对象相等 )

十三、hashCode()  与 equals()

        两个对象的 hashCode() 相同,equals() 不⼀定为 true。因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不⼀定能得出键值对相等【散列冲突】。

        需要用到 HashMap HashSet Java 集合,用不到哈希表的话,其实仅仅重写 equals() 方法也可以。而工作中的场景是常常用到 Java 集合,所以 Java 官方建议重写  equals() 就⼀定要重写 hashCode() 方 法。
        对于对象集合的判断,如果⼀个集合含有 10000 个对象实例,仅仅使用  equals() 方法的话,那么对于⼀个对象判断就需要比较 10000 次,随着集合规模的增大,时间开销是很大的。但是同时使用哈希表的话,就能快速定位到对象的大概存储位置,并且在定位到⼤概存储位置后,后续比较过程中,如果两个对象的 hashCode 不相同,也不再需要调⽤ equals() 方 法,从而大大减少了e quals() 比 较次数。 所以从程序实现原理上来讲的话,既需要 equals() 方 法,也需要 hashCode() 方 法。那么既然重写了 equals(),那么也要重写 hashCode() 方 法,以保证两者之间的配合关系。
        Java 中,Object 对象的 hashCode() 方法会根据不同的对象生成不同的哈希值,默认情况下为了确保这个哈希值的唯一性,是通过 该对象的内部 地址转换成一个整数来实现的
  •  一致性(consistent),在程序的一次执行过程中,对同一个对象必须一致地返回同一个整数。

  • 如果两个对象通过equals(Object)比较,结果相等,那么对这两个对象分别调用hashCode方法应该产生相同的整数结果。(PS:这里equalshashCode说的都是Object类的)

  • 如果两个对象通过java.lang.Object.equals(java.lang.Ojbect)比较,结果不相等,不必保证对这两个对象分别调用hashCode也返回两个不相同的整数。

hashCode ()与 equals ()的相关规定
        1、如果两个对象相等,则 hashCode ⼀定也是相同的;
        2、两个对象相等,对两个对象分别调用  equals 方 法都返回 true
        3、两个对象有相同的 hashCode 值,它们也不⼀定是相等的;
        4、因此, equals 方 法被覆盖过,则 hashCode 方 法也必须被覆盖;
        5、 hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode() ,则该 class 的两个对象无论如何都不会相等(不会产生相同的hash值)(即使这两个对象指向相同的数据)。
        hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数。这个哈希码的 作用是确定该对象在哈希表中的索引位置。 hashCode() 定义在 JDK 的 Object 类中,这就意味 着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用  c 语⾔或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
        散列表存储的是键值对(key-value) ,它的特点是:能根据 快速的检索出对应的 ”。这其中就 利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode
        我们以“ HashSet 如何检查重复 为例子来说明为什么要有 hashCode
        当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加入的位置, 同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让其加⼊操作成 功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提⾼了执行速度。
为什么重写 equals 时必须重写 hashCode ⽅法?
        如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等 , 对两个对象分别调用 equals方法都返回 true 。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此, equals 方 法被覆盖过,则 hashCode 方 法也必须被覆盖。
        hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode() ,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
为什么两个对象有相同的 hashcode 值,它们也不⼀定是相等的?
        因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算 法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同 的 hashCode
        我们刚刚也提到了 HashSet , 如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会 使用  equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
& && 的区别?
        Java 中 && & 都是表示与的逻辑运算符,都表示逻辑运输符 and ,当两边的表达式都为 true 的时候,整个运算 结果才为 true ,否则为 false
        &&:有短路功能,当第⼀个表达式的值为 false 的时候,则不再计算第⼆个表达式;
        &:不管第⼀个表达式结果是否为 true ,第⼆个都会执⾏。除此之外, & 还可以用作位运算符:当 & 两边的表达式不是 Boolean 类型的时候, & 表示按位操作。
Java 中的参数传递时传值呢?还是传引用?
        Java 的参数是以值传递的形式传入方法中,而不是引用传递。
        当传递方法参数类型为基本数据类型(数字以及布尔值)时,⼀个方法是不可能修改⼀个基本数据类型的参数。
        当传递方法参数类型为引用数据类型时,⼀个方法将修改⼀个引用数据类型的参数所指向对象的值。即使 Java 函数在传递引用数据类型时,也只是拷贝了引用的值罢了,之所以能修改引用数据是因为它们同时指向了⼀个对象,但这仍然是按值调用而不是引用调用。
Java 中的 Math.round(-1.5) 等于多少?
        等于 -1 ,因为在数轴上取值时,中间值( 0.5 )向右取整,所以正 0.5 是往上取整,负 0.5 是直接舍弃。

十四、实现对象的克隆,深拷贝和浅拷贝

如何实现对象的克隆?
        (1 )实现 Cloneable 接⼝并重写 Object 类中的 clone() ⽅法;
        (2 )实现 Serializable 接⼝,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。
深克隆和浅克隆的区别?
        (1)浅克隆:拷贝对象和原始对象的引用类型引用同⼀个对象。浅克隆只是复制了对象的引用地址,两个对象指向同⼀个内存地址,所以修改其中任意的值,另⼀个值都会随之变化,这就是浅克隆。
        (2)深克隆:拷贝对象和原始对象的引用类型引用不同对象。深拷贝是将对象及值复制过来,两个对象修改其中任意的值另⼀个值不会改变,这就是深拷贝(例: JSON.parse() JSON.stringify(),但是此方法无法复制函数类型)。
补充
        深克隆的实现就是在引用类型所在的类实现 Cloneable 接⼝,并使用  public 访问修饰符重写 clone 方 法。
        Java 中定义的 clone 没有深浅之分,都是统⼀的调用  Object clone ⽅法。为什么会有深克隆的概念?是由于我们在实现的过程中刻意的嵌套了 clone 方法的调用。也就是说深克隆就是在需要克隆的对象类型的类中重新实现克 隆方法 clone()
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建⼀个新的对象,并复制其内容,此为深拷贝。
字节面试杂谈——JAVA基础_第4张图片

十五、JAVA序列化

        对象序列化是⼀个用于将对象状态转换为字节流的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序。从字节流创建对象的相反的过程称为反序列化。而创建的字节流是与平台无关的,在⼀个平台上序列化的对象可以在不同的平台上反序列化。序列化是为了解决在对象流进行读写操作时所引发的问题。
        序列化的实现:将需要被序列化的类实现 Serializable 接⼝,该接⼝没有需要实现的⽅法,只是用于标注该对象是可被序列化的,然后使用⼀个输出流(如: FileOutputStream )来构造⼀个 ObjectOutputStream 对象,接着使用  ObjectOutputStream 对象的 writeObject(Object obj) 方 法可以将参数为 obj 的对象写出,要恢复的话则使用输⼊流。
什么情况下需要序列化?
1 )当你想把的内存中的对象状态保存到⼀个文件中或者数据库中时候;
2 )当你想用套接字在网络上传送对象的时候;
3 )当你想通过 RMI 传输对象的时候。
Java 序列化中如果有些字段不想进行序列化,怎么办?
        对于不想进行序列化的变量,使用 transient 关键字修饰。
        transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。 transient 只能修饰变量,不能修饰类和方法。

十六、反射

        Java 中的反射是什么意思?有哪些应用场景?
                每个类都有⼀个 Class 对象,包含了与类有关的信息。当编译⼀个新类时,会产生⼀个同名的 .class 文件,该文件内容保存着 Class 对象。类加载相当于 Class 对象的加载,类在第⼀次使用时才动态加载到 JVM 中。也可以使用Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回⼀个 Class 对象。
                反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以 加载进来。 Class java.lang.reflect ⼀起对反射提供了⽀持, java.lang.reflect 类库主要包含了以下三个类:
        (1 Field :可以使用  get() set() 方 法读取和修改 Field 对象关联的字段;
        (2 Method :可以使用  invoke() 方 法调用与 Method 对象关联的方法;
        (3 Constructor :可以用  Constructor 创建新的对象。
        应用举例:工厂模式,使用反射机制,根据全限定类名获得某个类的 Class 实例。
反射的优缺点?
        优点:
                运行期类型的判断,class.forName() 动态加载类,提⾼代码的灵活度;
        缺点:
                尽管反射非常强大,但也不能滥用。如果⼀个功能可以不用反射完成,那么最好就不⽤。在我们使用反射技术时,下面几条内容应该牢记于行。
        (1 )性能开销 :反射涉及了动态类型的解析,所以 JVM ⽆法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
        (2)安全限制 :使用反射技术要求程序必须在⼀个没有安全限制的环境中运行。如果⼀个程序必须在有安全限制 的环境中运行,如 Applet ,那么这就是个问题了。
        (3)内部暴露:由于反射允许代码执行⼀些在正常情况下不被允许的操作(比如:访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发⽣改变的时候,代码的行为就有可能也随着变化

(一)Class类

        Class是一个类,封装了当前对象所对应的类的信息

        对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个类的有关信息。 

        Class 对象只能由系统建立对象,一个类(而不是一个对象)在 JVM 中只会有一个Class实例

获取Class对象的三种方式:

        1.通过类名获取      类名.class    

   2.通过对象获取      对象名.getClass()

   3.通过全类名获取    Class.forName(全类名)

Class类的常用方法:

方法名

功能说明
static Class forName(String name) 返回指定类名 name 的 Class 对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例
Object newInstance(Object []args) 调用当前格式构造函数,返回该Class对象的一个实例
getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型或void)名称
Class getSuperClass() 返回当前Class对象的父类的Class对象
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class

(二)ClassLoader

        类装载器是用来把类(class)装载进 JVM 的。

public class ReflectionTest {
    @Test
    public void testClassLoader() throws ClassNotFoundException, FileNotFoundException{
        //1. 获取一个系统的类加载器(可以获取,当前这个类PeflectTest就是它加载的)
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        System.out.println(classLoader);
        
        //2. 获取系统类加载器的父类加载器(扩展类加载器,可以获取). 
        classLoader = classLoader.getParent();
        System.out.println(classLoader); 
        
        //3. 获取扩展类加载器的父类加载器(引导类加载器,不可获取).
        classLoader = classLoader.getParent();
        System.out.println(classLoader);
        
        //4. 测试当前类由哪个类加载器进行加载(系统类加载器): 
        classLoader = Class.forName("com.atguigu.java.fanshe.ReflectionTest")
             .getClassLoader();
        System.out.println(classLoader);
    
        //5. 测试 JDK 提供的 Object 类由哪个类加载器负责加载(引导类)
        classLoader = Class.forName("java.lang.Object")
                 .getClassLoader();
        System.out.println(classLoader); 
    }
}
//结果:
//sun.misc.Launcher$AppClassLoader@5ffdfb42
//sun.misc.Launcher$ExtClassLoader@1b7adb4a
//null
//sun.misc.Launcher$AppClassLoader@5ffdfb42
//null

(三)反射

        Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的內部信息,并能直接操作任意对象的内部属性及方法。

  Java反射机制主要提供了以下功能:

  • 在运行时构造任意一个类的对象
  • 在运行时获取任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法(属性)
  • 生成动态代理

  Class 是一个类; 一个描述类的类.

  封装了描述方法的 Method,

       封装了描述字段的 Filed,

       封装了描述构造器的 Constructor 等属性.

(1)Method

public class ReflectionTest {
    @Test
    public void testMethod() throws Exception{
        
        Class clazz = Class.forName("com.atguigu.java.fanshe.Person");
        
        //        
        //1.获取方法      
        //  1.1 获取取clazz对应类中的所有方法--方法数组(一)
        //     不能获取private方法,且获取从父类继承来的所有方法(不包括父类的私有方法)
        Method[] methods = clazz.getMethods();
        for(Method method:methods){
            System.out.print(" "+method.getName());
        }
        System.out.println();
        

        //
        //  1.2.获取所有方法,包括私有方法 --方法数组(二)
        //  所有声明的方法,都可以获取到,且只获取当前类的方法
        methods = clazz.getDeclaredMethods();
        for(Method method:methods){
            System.out.print(" "+method.getName());
        }
        System.out.println();
        

        //
        //  1.3.获取指定的方法
        //  需要参数名称和参数列表,无参则不需要写
        //  对于方法public void setName(String name) {  }
        Method method = clazz.getDeclaredMethod("setName", String.class);
        System.out.println(method);
        //  而对于方法public void setAge(int age) {  }
        method = clazz.getDeclaredMethod("setAge", Integer.class);
        System.out.println(method);
        //  这样写是获取不到的,如果方法的参数类型是int型
        //  如果方法用于反射,那么要么int类型写成Integer: public void setAge(Integer age){}     
        //  要么获取方法的参数写成int.class
        
        //
        //  2.执行方法
        //  invoke第一个参数表示执行哪个对象的方法,剩下的参数是执行方法时需要传入的参数
        Object obje = clazz.newInstance();
        method.invoke(obje,2);
    //如果一个方法是私有方法,第三步是可以获取到的,但是这一步却不能执行    
       //私有方法的执行,必须在调用invoke之前加上一句  method.setAccessible(true);    
    }
}

setAccessible()

        将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查;实际上setAccessible是启用和禁用访问安全检查的开关,并不是为true就能访问为false就不能访问 ;

        由于JDK的安全检查耗时较多.所以通过setAccessible(true)的方式关闭安全检查就可以达到提升反射速度的目的 

主要用到两个方法:

        /**
         * @param name the name of the method
         * @param parameterTypes the list of parameters
         * @return the {@code Method} object that matches the specified
         */
        public Method getMethod(String name, Class... parameterTypes){
            
        }
        
        /**
         * @param obj  the object the underlying method is invoked from
         * @param args the arguments used for the method call
         * @return  the result of dispatching the method represented by
         */
        public Object invoke(Object obj, Object... args){
            
        }

(2)Field

@Test
    public void testField() throws Exception{
        String className = "com.atguigu.java.fanshe.Person";        
        Class clazz = Class.forName(className); 
        
        //1.获取字段
        //  1.1 获取所有字段 -- 字段数组
        //     可以获取公用和私有的所有字段,但不能获取父类字段
        Field[] fields = clazz.getDeclaredFields();
        for(Field field: fields){
            System.out.print(" "+ field.getName());
        }
        System.out.println();
        

        //  1.2获取指定字段
        Field field = clazz.getDeclaredField("name");
        System.out.println(field.getName());
        
        Person person = new Person("ABC",12);
        

        //2.使用字段
        //  2.1获取指定对象的指定字段的值
        Object val = field.get(person);
        System.out.println(val);
        

        //  2.2设置指定对象的指定对象Field值
        field.set(person, "DEF");
        System.out.println(person.getName());
        

        //  2.3如果字段是私有的,不管是读值还是写值,都必须先调用setAccessible(true)方法
        //     比如Person类中,字段name字段是公用的,age是私有的
        field = clazz.getDeclaredField("age");
        field.setAccessible(true);
        field.set(person, 13);
        System.out.println(field.get(person));        
    }

(3)Constructor

    @Test
    public void testConstructor() throws Exception{
        String className = "com.atguigu.java.fanshe.Person";
        Class clazz = (Class) Class.forName(className);
        
        //1. 获取 Constructor 对象
        //   1.1 获取全部
        Constructor [] constructors = 
                (Constructor[]) Class.forName(className).getConstructors();
        
        for(Constructor constructor: constructors){
            System.out.println(constructor); 
        }
        
        //  1.2获取某一个,需要参数列表
        Constructor constructor = clazz.getConstructor(String.class, int.class);
        System.out.println(constructor); 
        
        //2. 调用构造器的 newInstance() 方法创建对象
        Object obj = constructor.newInstance("zhagn", 1);                
    }

(4)总结:

         1. Class: 是一个类; 一个描述类的类.

          封装了描述方法的 Method,

                描述字段的 Filed,

                          描述构造器的 Constructor 等属性.
 
         2. 如何得到 Class 对象:
            2.1 Person.class
            2.2 person.getClass()
            2.3 Class.forName("com.atguigu.javase.Person")
  
         3. 关于 Method:
            3.1 如何获取 Method:
              1). getDeclaredMethods: 得到 Method 的数组.
              2). getDeclaredMethod(String methondName, Class ... parameterTypes)
  
            3.2 如何调用 Method
              1). 如果方法时 private 修饰的, 需要先调用 Method 的 setAccessible(true), 使其变为可访问
              2). method.invoke(obj, Object ... args);
  
          4. 关于 Field:
            4.1 如何获取 Field: getField(String fieldName)
            4.2 如何获取 Field 的值: 
              1). setAccessible(true)
              2). field.get(Object obj)
            4.3 如何设置 Field 的值:
              field.set(Obejct obj, Object val)
  
          5. 了解 Constructor 和 Annotation 
  
          6. 反射和泛型.
            6.1 getGenericSuperClass: 获取带泛型参数的父类, 返回值为: BaseDao
            6.2 Type 的子接口: ParameterizedType
            6.3 可以调用 ParameterizedType 的 Type[] getActualTypeArguments() 获取泛型参数的数组.

十七、动态代理

        动态代理:当想要给实现了某个接口的类中的方法,加⼀些额外的处理。比如说加日志,加事务等。可以给这个类 创建⼀个代理,故名思议就是创建⼀个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新功能。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活,扩展性强。
        动态代理的应用:Spring AOP 、加事务、加权限、加日志。
怎么实现动态代理?
        首先必须定义⼀个接口,还要有⼀个 InvocationHandler (将实现接⼝的类的对象传递给它)处理类。
        再有⼀个工具类 Proxy (习惯性将其称为代理类,因为调用它的 newInstance() 可以产生代理对象,其实它只是⼀个产生代理 对象的工具类)。利⽤到 InvocationHandler,拼接代理类源码,将其编译生成代理类的⼆进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。
        
        每⼀个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实例都关联到了⼀个 handler, 当我们通过代理对象调用⼀个方法的时候,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke 方 法来进行调用。我们来看看 InvocationHandler 这个接口的唯⼀⼀个方法 invoke 方法:
                 Object invoke ( Object proxy , Method method , Object [] args ) throws Throwable loader
                        proxy: 指代我们所代理的那个真实对象
                        method: 指代的是我们所要调用真实对象的某个方法的 Method 对象
                        args: 指代的是调用真实对象某个方法时接受的参数
        
        Proxy 类的作用是动态创建⼀个代理对象的类。它提供了许多的⽅法,但是我们用的最多的就newProxyInstance 这个⽅法:
                 public static Object newProxyInstance (ClassLoader loader , Class [] interfaces ,
InvocationHandler handler ) throws IllegalArgumentException
                ⼀个 ClassLoader 对象,定义了由哪个 ClassLoader 对象来对生成的代理对象进行加载;
                interfaces:⼀个 Interface 对象的数组,表示的是我将要给我需要代理的对象提供⼀组什么接⼝,如果我提供了⼀ 组接⼝给它,那么这个代理对象就宣称实现了该接⼝ ( 多态 ) ,这样我就能调用这组接⼝中的方法了
                handler:⼀个 InvocationHandler 对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪⼀个 InvocationHandler 对象上。
                通过 Proxy.newProxyInstance 创建的代理对象是在 Jvm 运⾏时动态⽣成的⼀个对象,它并不是我们的 InvocationHandler 类型,也不是我们定义的那组接⼝的类型,而是在运行是动态⽣成的⼀个对象。
代理模式:
        代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。简单的说就是,我们在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性。

字节面试杂谈——JAVA基础_第5张图片

如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:

  • 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
  • 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件
(一)静态代理
(1)编写一个接口 UserService ,以及该接口的一个实现类 UserServiceImpl
public interface UserService {
    public void select();   
    public void update();
}

public class UserServiceImpl implements UserService {  
    public void select() {  
        System.out.println("查询 selectById");
    }
    public void update() {
        System.out.println("更新 update");
    }
}
(2)我们将通过静态代理对 UserServiceImpl 进行功能增强,在调用  select 和  update 之前记录一些日志。写一个代理类 UserServiceProxy,代理类需要实现 UserService
public class UserServiceProxy implements UserService {
    private UserService target; // 被代理的对象

    public UserServiceProxy(UserService target) {
        this.target = target;
    }
    public void select() {
        before();
        target.select();    // 这里才实际调用真实主题角色的方法
        after();
    }
    public void update() {
        before();
        target.update();    // 这里才实际调用真实主题角色的方法
        after();
    }

    private void before() {     // 在执行方法之前执行
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    private void after() {      // 在执行方法之后执行
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}
(3)客户端测试
public class Client1 {
    public static void main(String[] args) {
        UserService userServiceImpl = new UserServiceImpl();
        UserService proxy = new UserServiceProxy(userServiceImpl);

        proxy.select();
        proxy.update();
    }
}
(4)输出
log start time [Thu Dec 20 14:13:25 CST 2018] 
查询 selectById
log end time [Thu Dec 20 14:13:25 CST 2018] 
log start time [Thu Dec 20 14:13:25 CST 2018] 
更新 update
log end time [Thu Dec 20 14:13:25 CST 2018] 

        通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。

静态代理的缺点

虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。

1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:

  • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
  • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类

2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护

为什么类可以动态的生成?

这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。

Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:

  • 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
  • 从网络中获取,典型的应用是 Applet
  • 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流
  • 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
  • 从数据库中获取等等

所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。

(二)JDK动态代理

        JDK动态代理主要涉及两个类: java.lang.reflect.Proxy 和  java.lang.reflect.InvocationHandler,我们仍然通过案例来学习
(1)编写一个调用逻辑处理器 LogHandler 类,提供日志增强功能,并实现 InvocationHandler 接口;在 LogHandler 中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在  invoke 方法中编写方法调用的逻辑处理
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

public class LogHandler implements InvocationHandler {
    Object target;  // 被代理的对象,实际的方法执行者

    public LogHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);  // 调用 target 的 method 方法
        after();
        return result;  // 返回方法的执行结果
    }
    // 调用invoke方法之前执行
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    // 调用invoke方法之后执行
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

(2)编写客户端,获取动态生成的代理类的对象须借助 Proxy 类的 newProxyInstance 方法,具体步骤可见代码和注释
import proxy.UserService;
import proxy.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Client2 {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        // 设置变量可以保存动态代理类,默认名称以 $Proxy0 格式命名
        // System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 1. 创建被代理的对象,UserService接口的实现类
        UserServiceImpl userServiceImpl = new UserServiceImpl();
        // 2. 获取对应的 ClassLoader
        ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
        // 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService,
        Class[] interfaces = userServiceImpl.getClass().getInterfaces();
        // 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
        //     这里创建的是一个自定义的日志处理器,须传入实际的执行对象 userServiceImpl
        InvocationHandler logHandler = new LogHandler(userServiceImpl);
        /*
		   5.根据上面提供的信息,创建代理对象 在这个过程中,
               a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码
               b.然后根据相应的字节码转换成对应的class,
               c.然后调用newInstance()创建代理实例
		 */
        UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
        // 调用代理的方法
        proxy.select();
        proxy.update();
        
        // 保存JDK动态代理生成的代理类,类名保存为 UserServiceProxy
        // ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");
    }
}

(3)运行结果

log start time [Thu Dec 20 16:55:19 CST 2018] 
查询 selectById
log end time [Thu Dec 20 16:55:19 CST 2018] 
log start time [Thu Dec 20 16:55:19 CST 2018] 
更新 update
log end time [Thu Dec 20 16:55:19 CST 2018] 

(4)

InvocationHandler 和 Proxy 的主要方法介绍如下:

java.lang.reflect.InvocationHandler

Object invoke(Object proxy, Method method, Object[] args) 定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用

java.lang.reflect.Proxy

static InvocationHandler getInvocationHandler(Object proxy) 用于获取指定代理对象所关联的调用处理器

static Class getProxyClass(ClassLoader loader, Class... interfaces) 返回指定接口的代理类

static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法

static boolean isProxyClass(Class cl) 返回 cl 是否为一个代理类

(5)

从 UserServiceProxy 的代码中我们可以发现:

  • UserServiceProxy 继承了 Proxy 类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法
  • 由于 UserServiceProxy 继承了 Proxy 类,所以每个代理类都会关联一个 InvocationHandler 方法调用处理器
  • 类和所有方法都被 public final 修饰,所以代理类只可被使用,不可以再被继承
  • 每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以 m + 数字 的格式命名
  • 调用方法的时候通过 super.h.invoke(this, m1, (Object[])null); 调用,其中的 super.h.invoke 实际上是在创建代理的时候传递给 Proxy.newProxyInstance 的 LogHandler 对象,它继承 InvocationHandler 类,负责实际的调用处理逻辑

而 LogHandler 的 invoke 方法接收到 method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 target 执行方法

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);       // 调用 target 的 method 方法
        after();
        return result;  // 返回方法的执行结果
    }

(6)

描述动态代理的几种实现方式?分别说出相应的优缺点

代理可以分为 "静态代理" 和 "动态代理",动态代理又分为 "JDK动态代理" 和 "CGLIB动态代理" 实现。

静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object

  • 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
  • 缺点:不同的接口要有不同的代理类实现,会很冗余

JDK 动态代理

  • 为了解决静态代理中,生成大量的代理类造成的冗余;

  • JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,

  • jdk的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象

  • jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口

  • 优点:解决了静态代理中冗余的代理实现类问题。

  • 缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。

CGLIB 代理

  • 由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;

  • CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。

  • 实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。

  • 但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。

  • 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。

  • 优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。

  • 缺点:技术实现相对难理解些。

十八、字节与字符、String为什么不可变。

字节和字符的区别?
        字节是存储容量的基本单位;
        字符是数字、字母、汉字以及其他语⾔的各种符号;
        1 字节 = 8 个⼆进制单位,⼀个字符由⼀个字节或多个字节的⼆进制单位组成。
字符型常量和字符串常量的区别 ?
        1. 形式上 : 字符常量是单引号引起的⼀个字符 ; 字符串常量是双引号引起的若干个字符
        2. 含义上 : 字符常量相当于⼀个整型值 ( ASCII ), 可以参加表达式运算 ; 字符串常量代表⼀个地 址值 ( 该字符串在内存中存放位置 )
        3. 占内存大小: 字符常量只占 2 个字节 ; 字符串常量占若⼲个字节 ( 注意: char Java 中占两 个字节 )
字节面试杂谈——JAVA基础_第6张图片

String 为什么要设计为不可变类?
        在 Java 中将 String 设计成不可变的是综合考虑到各种因素的结果。主要的原因主要有以下三点:
        (1 )字符串常量池的需要:字符串常量池是 Java 堆内存中⼀个特殊的存储区域 , 当创建⼀个 String 对象时,假如此字符串值已经存在于常量池中,则不会创建⼀个新的对象,而是引用已经存在的对象;
        (2 )允许 String 对象缓存 HashCode Java String 对象的哈希码被频繁地使用 , 比 如在 HashMap 等容器中。 字符串不变性保证了 hash 码的唯⼀性,因此可以放心地进行缓存。这也是⼀种性能优化手段,意味着不必每次都 去计算新的哈希码;
        (3 String 被许多的 Java ( )用 来当做参数,例如:网络连接地址 URL 、⽂件路径 path、还有反射机制所需要 的 String 参数等 , 假若 String 不是固定不变的,将会引起各种安全隐患。

十九、String,StringBuilder,StringBuffer

        String:用于字符串操作,属于不可变类;【补充: String 不是基本数据类型,是引用类型,底层⽤ char 数组实现的】
        StringBuilder:与 StringBuffer 类似,都是字符串缓冲区,但线程不安全;
        StringBuffer:也⽤于字符串操作,不同之处是 StringBuffer 属于可变类,对方法加了同步锁,线程安全
        StringBuffer的补充
        说明:StringBuffer 中并不是所有方法都使⽤了 Synchronized 修饰来实现同步:
        
@Overridepublic StringBuffer insert(int dstOffset, CharSequence s) {

    // Note, synchronization achieved via invocations of other StringBuffer methods
    // after narrowing of s to specific type
    // Ditto for toStringCache clearing
    super.insert(dstOffset, s);
    return this;
}
         执行效率:StringBuilder > StringBuffer > String
(1)String 字符串修改实现的原理?
        当⽤ String 类型来对字符串进行修改时,其实现方法是首先创建⼀个 StringBuffer ,其次调用  StringBuffer 的 append() 方 法,最后调用  StringBuffer toString() 方 法把结果返回。
(2)String str = "i" String str = new String("i") ⼀样吗?
        不⼀样,因为内存的分配⽅式不⼀样。String str = "i" 的⽅式, Java 虚拟机会将其分配到常量池中;而 String str = new String("i") 则会被分到堆内存中。
public class StringTest {

    public static void main(String[] args) {

        String str1 = "abc";
        String str2 = "abc";
        String str3 = new String("abc");
        String str4 = new String("abc");
        System.out.println(str1 == str2); // true
        System.out.println(str1 == str3); // false
        System.out.println(str3 == str4); // false
        System.out.println(str3.equals(str4)); // true
    }
}
                在执行String str1 = "abc" 的时候,JVM 会首先检查字符串常量池中是否已经存在该字符串对象,如果已经存在, 那么就不会再创建了,直接返回该字符串在字符串常量池中的内存地址;如果该字符串还不存在字符串常量池中, 那么就会在字符串常量池中创建该字符串对象,然后再返回。所以在执⾏ String str2 = "abc" 的时候,因为字符串 常量池中已经存在 “abc” 字符串对象了,就不会在字符串常量池中再次创建了,所以栈内存中 str1 str2 的内存地 址都是指向 "abc" 在字符串常量池中的位置,所以 str1 = str2 的运行结果为 true
                而在执行 String str3 = new String("abc") 的时候, JVM 会首先检查字符串常量池中是否已经存在 “abc”字符串,如果已经存在,则不会在字符串常量池中再创建了;如果不存在,则就会在字符串常量池中创建 "abc" 字符串对象, 然后再到堆内存中再创建⼀份字符串对象,把字符串常量池中的 "abc" 字符串内容拷贝到内存中的字符串对象中, 然后返回堆内存中该字符串的内存地址,即栈内存中存储的地址是堆内存中对象的内存地址。String str4 = new String("abc") 是在堆内存中又创建了⼀个对象,所以 str3 == str4 运⾏的结果是 false str1 str2 str3 str4
内存中的存储状况如下图所示:
字节面试杂谈——JAVA基础_第7张图片

(3)String 类的常用方法都有那些?
        indexOf():返回指定字符的索引。
        charAt():返回指定索引处的字符。
        replace():字符串替换。
        trim():去除字符串两端空⽩。
        split():分割字符串,返回⼀个分割后的字符串数组。
        getBytes():返回字符串的 byte 类型数组。
        length():返回字符串⻓度。
        toLowerCase():将字符串转成⼩写字⺟。
        toUpperCase():将字符串转成⼤写字符。
        substring():截取字符串。
        equals():字符串⽐较。
(4)final 修饰 StringBuffer 后还可以 append 吗?
        可以。final 修饰的是⼀个引用变量,那么这个引用始终只能指向这个对象,但是这个对象内部的属性是可以变化 的。
区别:
        可变性
                简单的来说:String 类中使用  final 关键字修饰字符数组来保存字符串, private final char
value[] ,一般来说我们访问不到value数组,所以 String 对象是不可变的。(利用反射可以访问到)在 Java 9 之后, String 类的实现改用 byte 数组存储字符串 private final byte[] value
                而 StringBuilder 与 StringBuffer 都继承自  AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串 char[]value 但是没有用  final 关键字修饰,所以这两种对象都是可
变的。
                StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实 现的。
        线程安全性
                String 中的对象是不可变的,也就可以理解为常量,线程安全。
                AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了⼀些字符串的基本操作,如 expandCapacity、 append insert indexOf 等公共方法。 StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。 StringBuilder 并没有对方法进行加同步锁,所以是⾮线程安全的。
        性能
                每次对 String 类型进⾏改变的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String 对象。
                StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是⽣成新的对象并改变对象引用。相同情况下使用  StringBuilder 相比使用  StringBuffer 仅能获得 10%~15% 左右的性能提
升,但却要冒多线程不安全的⻛险。
对于三者使用的总结:
        1. 操作少量的数据 : 适用  String
        2. 单线程操作字符串缓冲区下操作⼤量数据 : 适用  StringBuilder
        3. 多线程操作字符串缓冲区下操作⼤量数据 : 适用  StringBuffer

二十、异常:Error与Exception、运行时异常与受检异常、throw与throws

1 finally 块中的代码什么时候被执行?
        在 Java 语⾔的异常处理中, finally 块的作⽤就是为了保证无论出现什么情况, finally 块里的代码⼀定会被执行。
        由于程序执行 return 就意味着结束对当前函数的调用并跳出这个函数体,因此任何语句要执行都只能在 return 前 执行(除非碰到 exit 函数),因此 finally 块⾥的代码也是在 return 之前执行的。
        此外,如果 try-finally 或者 catch-finally 中都有 return ,那么 finally 块中的 return 将会覆盖别处的 return 语 句,最终返回到调用者那里的是 finally return 的值。
2 finally 是不是⼀定会被执行到?
        不⼀定。
        (1 )当程序进⼊ try 块之前就出现异常时,会直接结束,不会执行  finally 块中的代码;
        (2 )当程序在 try 块中强制退出时也不会去执⾏ finally 块中的代码,比如在 try 块中执行  exit方 法。
3 try-catch-finally 中,如果 catch return 了, finally 还会执行 吗?
        会。程序在执行到 return 时会⾸先将返回值存储在⼀个指定的位置,其次去执行  finally 块,最后再返回。因此, 对基本数据类型,在 finally 块中改变 return 的值没有任何影响,直接覆盖掉;而对引用类型是有影响的,返回的 是在 finally 对 前面  return 语句返回对象的修改值。
4 try-catch-finally 中那个部分可以省略?
        catch 和 finally 语句块可以省略其中一个,否则编译会报错。        
        
package constxiong.interview;
 
public class TestOmitTryCatchFinally {
 
    public static void main(String[] args) {
        omitFinally();
        omitCatch();
    }
    
    /**
     * 省略finally 语句块
     */
    public static void omitFinally() {
        try {
            int i = 0;
            i += 1;
            System.out.println(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 省略 catch 语句块
     */
    public static void omitCatch() {
        int i = 0;
        try {
            i += 1;
        } finally {
            i = 10;
        }
        System.out.println(i);
    }
}
        catch 可以省略。try 只适合处理运行时异常, try+catch 适合处理运行时异常 +普通异常。也就是说,如果你只用try 去处理普通异常却不加以 catch 处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须⽤ catch 显示声明以便进⼀步处理。而运行时异常在编译时没有如此规定,所以 catch 可以省略,你加上 catch 编译器也觉得无可厚非。
5 Error Exception 的区别?
        Error 类和 Exception 类的父类都是 Throwable 类。主要区别如下:
        Error 类: ⼀般是指与虚拟机相关的问题,如:系统崩溃、虚拟机错误、内存空间不足、方法调⽤栈溢出等。这类 错误将会导致应用程序中断,仅靠程序本身无法恢复和预防;
        Exception 类:分为运行时异常和受检查的异常。
6 、运行时异常与受检异常有何异同?
        运行时异常:如:空指针异常、指定的类找不到、数组越界、⽅法传递参数错误、数据类型转换错误。可以编译通过,但是⼀运行就停止了,程序不会自己处理;
        受检查异常:要么⽤ try … catch… 捕获,要么用  throws 声明抛出,交给父类处理。
7 throw throws 的区别?
        (1 throw :在方法体内部,表示抛出异常,由方法体内部的语句处理; throw 是具体向外抛出异常的动作,所以它抛出的是⼀个异常实例;
        (2 throws:在方法声明后⾯,表示如果抛出异常,由该方法的调用者来进行异常的处理;表示出现异常的可能性,并不⼀定会发⽣这种异常。
8 、常见的异常类有哪些?
        NullPointerException:当应⽤程序试图访问空对象时,则抛出该异常。
        SQLException:提供关于数据库访问错误或其他错误信息的异常。
        IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
        FileNotFoundException:当试图打开指定路径名表示的⽂件失败时,抛出此异常。
        IOException:当发⽣某种 I/O 异常时,抛出此异常。此类是失败或中断的 I/O 操作⽣成的异常的通⽤类。
        ClassCastException:当试图将对象强制转换为不是实例的⼦类时,抛出该异常。
        IllegalArgumentException:抛出的异常表明向⽅法传递了⼀个不合法或不正确的参数。
9 、主线程可以捕获到子线程的异常吗?
        线程设计的理念:“ 线程的问题应该线程自己本身来解决,而不要委托到外部
        正常情况下,如果不做特殊的处理,在主线程中是不能够捕获到子线程中的异常的。如果想要在主线程中捕获子线 程的异常,我们可以用如下的方式进行处理,使用  Thread 的静态方法
Thread . setDefaultUncaughtExceptionHandler ( new MyUncaughtExceptionHandle ());

二十一、主线程可以捕获到子线程的异常吗

线程设计的理念:“ 线程的问题应该线程自己本身来解决,而不要委托到外部
        正常情况下,如果不做特殊的处理,在主线程中是不能够捕获到子线程中的异常的。如果想要在主线程中捕获子线 程的异常,我们可以用如下的方式进行处理,使用  Thread 的静态方法
Thread . setDefaultUncaughtExceptionHandler ( new MyUncaughtExceptionHandle ());

二十二、JAVA中IO流的分类、常用的实现类、字节流和字符流、获取键盘输入

1 Java 中的 IO 流的分类?说出几个你熟悉的实现类?
        按功能来分:输⼊流(input )、输出流( output )。
        按类型来分:字节流 和 字符流。
        字节流:InputStream/OutputStream 是字节流的抽象类,这两个抽象类又派⽣了若干子类,不同的子类分别处理
        不同的操作类型。具体子类如下所示
字节面试杂谈——JAVA基础_第8张图片
字符流: Reader/Writer 是字符的抽象类,这两个抽象类也派⽣了若⼲⼦类,不同的⼦类分别处理不同的操作类
型。
字节面试杂谈——JAVA基础_第9张图片

2 、字节流和字符流有什么区别?
        字节流按 8 位传输,以字节为单位输⼊输出数据,字符流按 16 位传输,以字符为单位输⼊输出数据。
        但是不管文件读写还是网络发送接收,信息的最小存储单元都是字节。
3.获取用键盘输入常用的两种方法
        方法 1 :通过 Scanner


                Scanner input = new Scanner(System.in);
                String s = input.nextLine();
                input.close();

        ⽅法 2:通过 BufferedReader

                BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
                String s = input.readLine();

4.Java IO 流分为几种 ?
        按照流的流向分,可以分为输⼊流和输出流;
        按照操作单元划分,可以划分为字节流和字符流;
        按照流的角色划分为节点流和处理流。
        Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,⽽且彼此之间存在非常 紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派⽣出来的。
        InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
        OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:

字节面试杂谈——JAVA基础_第10张图片

 按操作对象分类结构图:

字节面试杂谈——JAVA基础_第11张图片

既然有了字节流 , 为什么还要有字符流 ?
        问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
        回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频⽂件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

二十三、BIO、NIO、AIO

        BIO: Block IO 同步阻塞式 IO ,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。 同步阻塞 I/O模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完成。在活动连接数不是特别⾼(小于单机 1000 )的情况下,这种模型是比较不错的,可以让每⼀个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是⼀个天然的漏⽃,可以缓冲⼀些系统处理不了的连接或请求。但是, 当⾯对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要⼀种更⾼效的 I/O 处理模型来应对更高的并发量。
        NIO: New IO 同步非阻塞 IO ,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。 NIO 是⼀种同步非阻塞的 I/O 模型,在 Java1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector Buffer 等抽象。 NIO 中的 N 可以理解为 Non-blocking ,不单纯是 New。它支持面向缓冲的,基于通道 的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传 统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程 序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应⽤,应使⽤ NIO 的 非阻塞模式来开发。
        AIO Asynchronous IO NIO 的升级,也叫 NIO2 ,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机 制。也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO IO 行为还是同步的。 对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自去进行  IO 操作, IO操作本身是同步的。
        BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写入必须阻塞在⼀个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以 让每⼀个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问 题。线程池本身就是⼀个天然的漏⽃,可以缓冲⼀些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要⼀种更高效的 I/O 处理模型来应对更⾼的并发量。
        NIO (Non-blocking/New I/O): NIO 是⼀种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了NIO 框架,对应 java.nio 包,提供了 Channel , Selector Buffer 等抽象。 NIO 中的 N 可以理解为 Non-blocking ,不单纯是 New 。它支持面向缓冲的,基于通道的 I/O 操作⽅法。
        NIO 提供了与传统 BIO 模型中的 Socket ServerSocket 相对应的 SocketChannel 和S erverSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。 阻塞模式使用就像传统中的支持⼀样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(⽹络)应用,应使用  NIO 的非阻塞模式来开发
        AIO (Asynchronous I/O): AIO 也就是 NIO 2 。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那⾥,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 AIO 是异步 IO 的缩写,虽然 NIO 在⽹络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就 由这个线程自行进行  IO 操作, IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很⼴泛, Netty 之前也尝试使用过 AIO ,不过又放弃了。

二十四、成员变量与局部变量

        1.从语法形式上看:
                成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;
                成员变量可以被 public , private , static 等修饰符所修饰,而局部变量不能被访问控制修饰 符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
        2. 从变量在内存中的存储方式来看 :
                如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引用数据类型,那存放的是指向堆内存对象的引用或者是指向常量池中的地址。
        3. 从变量在内存中的⽣存时间上看 :
                成员变量是对象的⼀部分,它随着对象的创建而存在,而局 部变量随着⽅法的调用而自动消失。
        4. 成员变量如果没有被赋初值 :
                则会自动以类型的默认值而赋值(⼀种情况例外: final 修饰 的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
创建⼀个对象用什么运算符 ? 对象实体与对象引用有何不同 ?
        new 运算符, new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。⼀个对象引用可以指向 0 个或 1 个对象(⼀根绳子可以不系气球,也可以系一个气球) ; ⼀个对象可以有 n 个引用指向它(可以用  n 条绳子系住⼀个气球)。
什么是方法的返回值 ? 返回值在类的方法里的作用是什么 ?
        ⽅法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用 : 接收出结果,使得它可以⽤于其他的操作!
⼀个类的构造方法的作用是什么 ? 若⼀个类没有声明构造方 法,该程序能正确执行吗 ? 为什么 ?
        主要作用是完成对类对象的初始化⼯作。可以执行。因为⼀个类即使没有声明构造方法也会有默认的不带参数的构造方法。
构造方法有哪些特性?
        1. 名字与类名相同。
        2. 没有返回值,但不能用  void 声明构造函数。
        3. 生成类的对象时自动执行,无需调用。

二十五、静态方法和实例方法

        1. 在外部调用静态方法时,可以使用 " 类名 .方 法名 " 的方式,也可以使用 " 对象名 .方 法名 " 的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
        2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态⽅法),而不 允许访问实例成员变量和实例⽅法;实例方法则无此限制。
对象的相等与指向他们的引用相等 , 两者有什么不同 ?
        对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
在调用子类构造方法之前会先调用父类没有参数的构造方法 , 其目的是 ?
        帮助子类做初始化⼯作。

二十六、JAVA中值传递

        按值调用 (call by value) 表示方法接收的是调用者提供的值,而按引用调用( call by reference) 表示方法 接收的是调用者提供的变量地址。
        ⼀个方法可以修改传递引用所对应的变量值,而不能修改传递 值调用所对应的变量值。
        Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的⼀个拷贝,也就 是说,方法不能修改传递给它的任何参数变量的内容。
        Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。
        下面再总结⼀下 Java 中方法参数的使用情况:
                ⼀个方法不能修改⼀个基本数据类型的参数(即数值型或布尔型)。
                ⼀个方法可以改变⼀个对象参数的状态。
                ⼀个方法不能让对象参数引用⼀个新的对象。

二十七、JAVA中线程的基本状态

        线程与进程相似,但线程是⼀个比进程更小的执行单位。⼀个进程在其执行的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享同⼀块内存空间和⼀组系统资源,所以系统在产生⼀个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
        程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
        进程是程序的⼀次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是⼀个进程从创建,运行到消亡的过程。简单来说,⼀个进程就是⼀个执行中的程序,它在计算机中⼀个指令接着⼀个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
        线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独里的,而各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。从另⼀⻆度来说,进程属于操作系统的范畴,主要是同⼀段时间内,可以同时执行⼀个以上的程序,而线程则是在同⼀程序内几乎
同时执行⼀个以上的程序段
字节面试杂谈——JAVA基础_第12张图片

线程在⽣命周期中并不是固定处于某⼀个状态而是随着代码的执行在不同状态之间切换

字节面试杂谈——JAVA基础_第13张图片
由上图可以看出:
        线程创建之后它将处于 NEW (新建) 状态,调用  start() ⽅法后开始运行,线程这时候处于 READY (可运行) 状态。可运行状态的线程获得了 cpu 时间片( timeslice)后就处于 RUNNING (运行) 状态。
字节面试杂谈——JAVA基础_第14张图片

        当线程执行 wait() 方 法之后,线程进入  WAITING (等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而  TIME_WAITING( 超时等待 ) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep long millis ⽅法或 wait long millis  方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED (阻塞) 状态。线程在执行  Runnable run() ⽅法之后将会进⼊到 TERMINATED (终止) 状态。

二十八、final关键字、static关键字

关于 final 关键字的⼀些总结
        final 关键字主要⽤在三个地方:变量、方法、类。
        1. 对于⼀个 final 变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更
改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。
        2. 用  final 修饰⼀个类时,表明这个类不能被继承。 final 类中的所有成员方法都会被隐式地
指定为 final 方 法。
        3. 使用  final 方法的原因有两个。第⼀个原因是把方法锁定,以防任何继承类修改它的含义;第⼆个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用final 方 法进行这些优化了)。类中所有的 private 方 法都隐式地指定为 final。

二十九、Throwable类常用方法

字节面试杂谈——JAVA基础_第15张图片

字节面试杂谈——JAVA基础_第16张图片

        在 Java 中,所有的异常都有⼀个共同的祖先 java.lang 包中的 Throwable 类。 Throwable
类 有两个重要的子类 Exception (异常)和 Error (错误)。 Exception 能被程序本身处理(try-catch ), Error 是无法处理的 ( 只能尽量避免 )
Exception 和 Error ⼆者都是 Java 异常处理的重要子类,各自都包含大量子类。
        Exception:程序本身可以处理的异常,可以通过 catch 来进行捕获。 Exception 又 可以分 为 受检查异常 ( 必须处理 ) 和 不受检查异常 ( 可以不处理 )
        Error Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如, Java 虚拟机运行错误( Virtual MachineError )、虚拟机内存不够错误 ( OutOfMemoryError ) 、类定义错误( NoClassDefFoundError )等 。这些异常发⽣时,Java 虚拟机( JVM )⼀般会选择线程终止。
受检查异常
        Java 代码在编译过程中,如果受检查异常没有被 catch / throw 处理的话,就没办法通过编译 。
不受检查异常
        Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。R untimeException 及其子类都统称为非受检查异常,例 如: NullPointExecrption 、 NumberFormatException (字符串转换为数字)、 ArrayIndexOutOfBoundsException (数组越界)、 ClassCastException (类型转换错误)、 ArithmeticException (算术错误)等。
Throwable 类常用方法
        public string getMessage() : 返回异常发生时的简要描述
        public string toString() : 返回异常发生时的详细信息
        public string getLocalizedMessage() :返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage 返回的结果相同
        public void printStackTrace() :在控制台上打印 Throwable 对象封装的异常信息
异常处理总结:
         catch 和 finally 语句块可以省略其中一个,否则编译会报错。     
        受检异常必须要有catch
        try 块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有catch 块,则必须跟 ⼀个finally 块。
        catch 块: 用于处理 try 捕获到的异常。
        finally 块: ⽆论是否捕获或处理异常, finally 块⾥的语句都会被执行。当在 try 块或
catch 块中遇到 return 语句时, finally 语句块将在方法返回之前被执行。
在以下 3 种特殊情况下, finally 块不会被执⾏行:
        1. 在 try 或 finally 块中调用了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常
语句之后,finally 还是会被执行
        2. 程序所在的线程死亡。
        3. 关闭 CPU

三十、try-catch-finally

try、catch、finally用法总结:

  1、不管有没有异常,finally中的代码都会执行

  2、当try、catch中有return时,finally中的代码依然会继续执行

  3、程序在执行到 return 时会首先将返回值存储在⼀个指定的位置,其次去执行 finally 块,最后再返回。因此, 对基本数据类型,在 finally 块中改变 return 的值没有任何影响,直接覆盖掉;而对引用类型是有影响的,返回的 是 finally 对 前面 return 语句返回对象的修改值。

  4、finally代码中最好不要包含return,程序会提前退出,也就是说返回的值不是try或catch中的值

三十一、匿名函数 Lambda 表达式

        Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。

        Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

        使用 Lambda 表达式可以使代码变的更加简洁紧凑。

语法

        lambda 表达式的语法格式如下:

        (parameters) -> expression 或 (parameters) ->{ statements; }

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。

Lambda 表达式实例

Lambda 表达式的简单例子:

// 1. 不需要参数,返回值为 5  
() -> 5  
  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)
public class Java8Tester {
   public static void main(String args[]){
      Java8Tester tester = new Java8Tester();
        
      // 类型声明
      MathOperation addition = (int a, int b) -> a + b;
        
      // 不用类型声明
      MathOperation subtraction = (a, b) -> a - b;
        
      // 大括号中的返回语句
      MathOperation multiplication = (int a, int b) -> { return a * b; };
        
      // 没有大括号及返回语句
      MathOperation division = (int a, int b) -> a / b;
        
      System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
      System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
      System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
      System.out.println("10 / 5 = " + tester.operate(10, 5, division));
        
      // 不用括号
      GreetingService greetService1 = message ->
      System.out.println("Hello " + message);
        
      // 用括号
      GreetingService greetService2 = (message) ->
      System.out.println("Hello " + message);
        
      greetService1.sayMessage("Runoob");
      greetService2.sayMessage("Google");
   }
    
   interface MathOperation {
      int operation(int a, int b);
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
    
   private int operate(int a, int b, MathOperation mathOperation){
      return mathOperation.operation(a, b);
   }
}
$ javac Java8Tester.java 
$ java Java8Tester
10 + 5 = 15
10 - 5 = 5
10 x 5 = 50
10 / 5 = 2
Hello Runoob
Hello Google

使用 Lambda 表达式需要注意以下两点:

  • Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
  • Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。

变量作用域

(1)lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。

public class Java8Tester {
 
   final static String salutation = "Hello! ";
   
   public static void main(String args[]){
      GreetingService greetService1 = message -> 
      System.out.println(salutation + message);
      greetService1.sayMessage("Runoob");
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
}
$ javac Java8Tester.java 
$ java Java8Tester
Hello! Runoob

(2)我们也可以直接在 lambda 表达式中访问外层的局部变量:

public class Java8Tester {
    public static void main(String args[]) {
        final int num = 1;
        Converter s = (param) -> System.out.println(String.valueOf(param + num));
        s.convert(2);  // 输出结果为 3
    }
 
    public interface Converter {
        void convert(int i);
    }
}

(3)ambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)

int num = 1;  
Converter s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
num = 5;  
//报错信息:Local variable num defined in an enclosing scope must be final or effectively 
 final

(4)在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

String first = "";  
Comparator comparator = (first, second) -> Integer.compare(first.length(), second.length());  //编译会出错 

三十二、获得Class对象的方法

Class类的特点:(结合截图理解)

  • 该类在java.lang包中;
  • 该类被final所修饰,即该类不可以被子类继承;
  • 该类实现了Serializable接口;
  • 该类的构造方法被private所修饰,即不能通过new关键字创建该类的对象;
public class Test {
 
	public static void main(String[] args) {
 
	Class clazz = null;
	
	try {

		//1、Class.forName()
		clazz = Class.forName("venus.Student");
		System.out.println("1-->"+clazz);

	    } catch (ClassNotFoundException e) {
		    
            e.printStackTrace();
	    
        }
		
		//2、类名.class
		clazz = Student.class;
		System.out.println("2-->"+clazz);
		
		//3、对象.getClass()
		clazz = new Student().getClass();
		System.out.println("3-->"+clazz);
		
		//4、基本数据类型对应的class对象:包装类.TYPE
		clazz = Integer.TYPE;
		System.out.println("4-->"+clazz);
		clazz = Integer.class;
		System.out.println("4-->"+clazz);
		
		//5、数组对应的class对象:元素类型[].class
		clazz = String[].class;
		System.out.println("5-->"+clazz);
		
		//6、某个类的父类所对应的class对象:类名.class.getSuperclass()
		clazz = Student.class.getSuperclass();
		System.out.println("6-->"+clazz);
	}
}

 (1)类、枚举、接口、注解、数组类型、原生类型的名称.class 
示例:

Class classString=String.class;//类
Class classEnum=RetentionPolicy.class;//枚举
Class classInterface=Serializable.class;//接口
Class classAnnotation=Retention.class;//注解
Class classInt=int.class;//原生类型
Class classIntArray=int[].class;//原生数组类型
Class classStringArray=String[].class;//数组类型

(2)对象.getClass() 
由于原生类型不是对象,所以无法使用getClass(),其他类型都是支持的。 
示例

Class classString = new String().getClass();// 类
Class classEnum = RetentionPolicy.SOURCE.getClass();// 枚举
Class classInterface = new Serializable() {}.getClass();// 接口
Class classAnnotation = new Documented() {public Class annotationType() {return null;}}.getClass();// 注解
// Class classInt=。。。;//原生类型不是对象,不能使用getClass()方法
Class classIntArray = new int[] {}.getClass();// 原生数组类型
Class classStringArray = new String[] {}.getClass();// 数组类型

(3)使用Class.forName 
Class.forName方法有两个: 
1.forName(String name) 
2.forName(String name, boolean initialize,ClassLoader loader)

forName(String name)其实调用的是forName(String name,boolean initialize,ClassLoader loader)

forName(className, true, ClassLoader.getCallerClassLoader());

boolean initialize参数很关键,如果为true,类会被初始化,静态变量会赋上初始值,静态代码块会被执行,如果为false则不会被初始化。

Class.forName仍然不支持原生类型(基本类型),但其他类型都是支持的。

Class classString = Class.forName("java.lang.String");// 类
Class classEnum = Class.forName("java.lang.annotation.RetentionPolicy");// 枚举
Class classInterface = Class.forName("java.io.Serializable");// 接口
Class classAnnotation = Class.forName("java.lang.annotation.Documented");// 注解
// Class classInt=。。。;//原生类型不是对象,不能使用Class.forName方法
Class classIntArray = Class.forName("[I");// 原生数组类型
Class classStringArray =  Class.forName("[Ljava.lang.String;");// 数组类型

(4)使用ClassLoader.loadClass 
此方法也能加载类,效果同Class.forName(className, false,ClassLoader.getCallerClassLoader()),不会初始化类。 
但ClassLoader.loadClass跟Class.forName相比,ClassLoader.loadClass不能对数组类型使用。 
除了原生类型和数组类型,其他类型都是支持的。 
示例:

Class classString = ClassLoader.getSystemClassLoader().loadClass("java.lang.String");// 类
Class classEnum =  ClassLoader.getSystemClassLoader().loadClass("java.lang.annotation.RetentionPolicy");// 枚举
Class classInterface =  ClassLoader.getSystemClassLoader().loadClass("java.io.Serializable");// 接口
Class classAnnotation =  ClassLoader.getSystemClassLoader().loadClass("java.lang.annotation.Documented");// 注解
//Class classInt=。。。;//原生类型不是对象,不能使用ClassLoader.loadClass方法
//Class classIntArray =  ClassLoader.getSystemClassLoader().loadClass("[I");// 数组类型不能使用ClassLoader.loadClass方法
//Class classStringArray =  ClassLoader.getSystemClassLoader().loadClass("[Ljava.lang.String;");// 数组类型不能使用ClassLoader.loadClass方法

三十三、java语言的特点

        1. 简单易学;
        2. 面向对象(封装,继承,多态);
        3. 平台无关性( Java 虚拟机实现平台无关性);
        4. 可靠性;
        5. 安全性;
        6. 支持多线程( C++ 语⾔没有内置的多线程机制,因此必须调⽤操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程⽀持);
        7. 支持网络编程并且很方便( Java 语言诞⽣本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);
        8. 编译与解释并存;

三十四、杂项

(1)一个java源文件中可以定义多个class。一个java源文件中public的class不是必须的。一个class会定义生成一个xxx.class字节码文件。一个java源文件当中定义公开的类的话,只能有一个并且该类名称必须和java源文件名称一致。

(2)变量在java中必须先定义再赋值才能访问

(3)变量作用域只需记住一句话——除了大括号就不认识了,注意for是一个整体

(4)java语言源代码采用的是Unicode编码方式,标识符可以用中文

(5)成员变量没有手动赋值系统会默认赋值,局部变量不会

(6)方法体中不能再定义方法

(7)java不支持默认参数。java不支持重载运算符。没有参数时形参直接省略,不能用void

(8)this是一个引用,this是一个变量,this变量中保存的内存地址指向了自身,this存储在JVM堆内存java对象内部。

(9)空引用也可以访问带有static的方法,不会产生空指针异常

(10)实例代码块可以编写多个,遵循自上而下的顺序执行。实例代码块在构造方法执行之前执行,构造方法执行一次,实例代码块对应执行一次。

(11)final是一个关键字,表示最终的,不可变的。
        final修饰的类无法被继承
        final修饰的方法无法被覆盖
        final修饰的局部变量一旦赋值之后,不可重新赋值
        final修饰的实例变量,因为实例变量有默认值,采用final修饰时必须手动赋值
        final修饰引用,一旦指向某个对象后不再断开,不能再指向其他对象。但指向的那个对象的内容可以改变

(12)
        public表示公开的,在任何位置都可以访问
        private表示私有的,只能在本类中访问
        protected同包,子类中可以访问
        缺省 同包下可以访问

(13)super不是引用类型,super中存储的不是内存地址,super指向的不是父类对象
        super代表的时当前子类对象中的父类型特征
        一个构造方法第一行如果没有this(),也没有显式的去调用super(),则系统会默认调用super()
        super()的调用只能放在构造方法的第一行,super()和this()不能共存
        super()调用了父类中的构造方法,但是并不会创建父类对象

(14)排序:
        实现Comparable接口,重写compareTo()方法
        实现Comparator接口,重写compare()方法(单独的排序器)

(15)优先队列PriorityQueue的默认大小为11,内部使用Object[]数组来实现。
        如果当前容量小于64,则扩容时+2
        如果当前容量大于等于64,则扩容时*1.5

你可能感兴趣的:(#,JAVA基础,面试,职场和发展)