基础
- 1、成员变量与局部变量的区别
- 2、静态变量有什么作用
- 3、字符型常量和字符串常量的区别
- 4、静态方法为什么不能调用非静态成员
- 5、静态方法和实例方法有何不同
- 6、重载和重写有什么区别
- 7、什么是可变长参数
- 8、Java 中的几种基本数据类型了解么
- 9、基本类型和包装类型的区别
- 10、包装类型的缓存机制了解么
- 11、自动装箱与拆箱了解吗,原理是什么
- 12、为什么浮点数运算的时候会有精度丢失的风险
- 13、如何解决浮点数运算的精度丢失问题
- 14、超过 long 整型的数据应该如何表示
- 15、创建一个对象用什么运算符,对象实体与对象引用有何不同
- 16、对象的相等和引用相等的区别
- 17、面向对象三大特征
- 18、接口和抽象类有什么共同点和区别
- 19、深拷贝和浅拷贝区别了解吗,什么是引用拷贝
- 20、Object 类的常见方法有哪些
- 21、== 和 equals() 的区别
- 22、为什么要有 hashCode
- 23、String、StringBuffer、StringBuilder 的区别
- 24、String 为什么是不可变的
- 25、字符串拼接用 + 还是 StringBuilder
- 26、String equals() 和 Object equals() 有何区别
- 27、字符串常量池的作用了解吗
- 28、String s1 = new String("abc")这句话创建了几个字符串对象
- 29、intern 方法有什么作用
- 30、String 类型的变量和常量做 + 运算时发生了什么
- 31、Exception 和 Error 有什么区别
- 32、Checked Exception 和 Unchecked Exception 有什么区别
- 33、Throwable 类常用方法有哪些
- 34、try-catch-finally 如何使用
- 35、finally 中的代码一定会执行吗
- 36、throw 和 throws 的区别
- 37、常见的异常类有哪些
- 38、什么是泛型,有什么作用
- 39、泛型的使用方式有哪几种
- 40、何谓反射
- 41、何谓注解
- 42、注解的解析方法有哪几种
- 43、什么是序列化,什么是反序列化
- 44、序列化协议对应于 TCP/IP 4 层模型的哪一层
- 45、如果有些字段不想进行序列化怎么办
- 46、常见序列化协议有哪些
- 47、为什么不推荐使用 JDK 自带的序列化
- 48、Java IO 流了解吗
- 49、I/O 流为什么要分为字节流和字符流呢
- 50、java 中都有哪些引用类型
- 51、BIO、NIO 和 AIO 的区别
- 52、当Java程序中用到的数值位数特别多时怎么办
- 53、int 和 Integer 有什么区别
- 54、Final、Finally、Finalize区别
- 55、实例化对象有哪几种方式
- 56、byte类型127+1等于多少
- 57、Java方法重写需要遵循什么原则?
- 58、Java的instanceof有什么用处
- 59、创建一个Java对象时,执行顺序是怎样的?
- 60、使用实体类作为入参的优缺点
- 61、使用 Map 作为入参的优缺点
- 62、Java基本类型的参数传递和引用类型的参数传递有啥区别?
- 63、Java中"hello"直接量和new String("hello")有什么区别呢?
- 64、Java final修饰基本类型变量和引用类型变量的区别?
- 65、Java中怎样的final变量可执行“宏替换”?
- 66、枚举类与普通类有什么区别?
- 67、Java枚举类不是用final修饰了吗?怎么还能派生子类呢?
- 68、使用Java的IO流执行输出时,为啥要关闭输出流?
- 69、Java如何使用Proxy和InvocationHandler创建动态代理?
- 70、举例说明什么情况下会更倾向于使用抽象类而不是接口
1、成员变量与局部变量的区别
- 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static
所修饰;但是,成员变量和局部变量都能被 final 所修饰。 - 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
- 生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
2、静态变量有什么作用
静态变量可以被类的所有实例共享。无论一个类创建了多少个对象,它们都共享同一份静态变量。通常情况下,静态变量会被 final 关键字修饰成为常量。
3、字符型常量和字符串常量的区别
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
- 含义 : 字符常量相当于一个整型值(ASCII 值),可以参加表达式运算,字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小 : 字符常量只占 2 个字节,字符串常量占若干个字节。
4、静态方法为什么不能调用非静态成员
这个需要结合 JVM 的相关知识,主要原因如下:
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
5、静态方法和实例方法有何不同
- 调用方式
在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
- 访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
6、重载和重写有什么区别
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理,重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
7、什么是可变长参数
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 printVariable 方法就可以接受 0 个或者多个参数。
public static void method1(String... args) {
//......
}
8、Java 中的几种基本数据类型了解么
Java 中有 8 种基本数据类型,分别为:6 种数字类型: 4 种整数型:byte、short、int、long2 种浮点型:float、double1 种字符类型:char1 种布尔型:boolean。
9、基本类型和包装类型的区别
- 成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
- 包装类型可用于泛型,而基本类型不可以。
- 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
- 相比于对象类型, 基本数据类型占用的空间非常小。
10、包装类型的缓存机制了解么
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 property
int 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_VALUE
h = 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
11、自动装箱与拆箱了解吗,原理是什么
什么是自动拆装箱
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
1.5 以前需要调用 valueOf() 方法手动装箱。使用 intValue() ,doubleValue() 等这类的方法手动拆箱。
1.5 以后才有自动装箱和拆箱,自动装箱时,编译器调用 valueOf 将原始类型值转换成对象。自动拆箱时,编译器通过调用类似intValue(),doubleValue()等这类的方法将对象转换成原始类型值。
12、为什么浮点数运算的时候会有精度丢失的风险
浮点数运算精度丢失代码演示:
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(发生循环)
...
13、如何解决浮点数运算的精度丢失问题
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 */
14、超过 long 整型的数据应该如何表示
基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。在 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));
}
}
15、创建一个对象用什么运算符,对象实体与对象引用有何不同
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
16、对象的相等和引用相等的区别
对象的相等一般比较的是内存中存放的内容是否相等。
引用相等一般比较的是他们指向的内存地址是否相等。
17、面向对象三大特征
Java也支持面向对象的三大特征:封装、继承和多态。
封装
java中提供了不同的封装级别:public、protected、默认的、private。
- public:公共的,可以修饰类,成员变量,成员方法,修饰的成员在任何场景中都可以访问。
- protected:受保护的,可以修饰成员变量和方法,修饰的成员在子类和同一个包中可以访问。
- 默认:不加任何修饰符,类,变量,方法。
- private:私有的,可以修饰成员变量和方法。
继承
提供了extends关键字来让子类继承父类,子类继承父类就可以继承到父类的Field和方法,如果访问控制允许,子类实例可以直接调用父类里定义的方法。继承是实现类复用的重要手段,除此之外,也可通过组合关系来实现这种复用,从某种程度上来看,继承和组合具有相同的功能。使用继承关系来实现复用时,子类对象可以直接赋给父类变量,这个变量具有多态性,编程更加灵活;而利用组合关系来实现复用时,则不具备这种灵活性。
多态
对面向对象来说,多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是大家通常所说的多态性。
Java 实现多态有 3 个必要条件:继承、重写和向上转型。只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。
引用类型的变量,定义为父类型,该变量就具备多态。
通过父类型的引用,调用父类型的方法,如果子类型重写了父类型的方法,父类型的引用指向了子类的对象,调用该方法就具备多态性。在程序中的多态,就是指同一个引用类型,使用不同的实例而执行不同的操作。
多态的意义:一个类型的引用指向不同的对象,会有不同的功能实现同一个对象,造型成不同的类型,会有不同的功能。
18、接口和抽象类有什么共同点和区别
共同点 :
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别 :
- 一个类只能继承一个类,但是可以实现多个接口。
- 抽象类中可以有成员变量,接口只能有静态常量。
19、深拷贝和浅拷贝区别了解吗,什么是引用拷贝
浅拷贝:克隆出来的数据并不能完全脱离原数据,克隆前与克隆后的变量各自的变化会相互影响。这是因为引用变量存储在栈中,而实际的对象存储在堆中。每一个引用变量都有一根指针指向其堆中的实际对象。即当一个变量值改变时,另一个变量也会跟着发生变化。
深拷贝:所有元素或属性均完全复制,与原对象完全脱离,也就是说所有对于新对象的修改都不会反映到原对象中。这是因为原始变量之间的赋值操作本质上就是当一个原始变量把值赋给另一个原始变量时,只是把栈中的内容复制给另一个原始变量,在这种操作下,引用变量指向的将不再是堆中的同一块地址,因此对于新对象的修改并不会影响到原对象。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
一张图来描述浅拷贝、深拷贝、引用拷贝:
20、Object 类的常见方法有哪些
/**
* 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 { }
21、== 和 equals() 的区别
- == 对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,== 比较的是值。
- 对于引用数据类型来说,== 比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
equals() 方法存在两种使用情况:
- 类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
- 类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
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
22、为什么要有 hashCode
我们以 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 )。
总结下来就是 :
- 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
- 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
- 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
23、String、StringBuffer、StringBuilder 的区别
- 可变性
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。
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder。
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer。
24、String 为什么是不可变的
String 类中使用 final 关键字修饰字符数组来保存字符串
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
String 真正不可变有下面几点原因:
- 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
- String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
25、字符串拼接用 + 还是 StringBuilder
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);
26、String equals() 和 Object equals() 有何区别
String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的 equals 方法是比较的对象的内存地址。
27、字符串常量池的作用了解吗
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
28、String s1 = new String(“abc”)这句话创建了几个字符串对象
会创建 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");
29、intern 方法有什么作用
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
30、String 类型的变量和常量做 + 运算时发生了什么
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"; 。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
- final 修饰的基本数据类型和字符串变量
- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
对象引用和“+”的字符串拼接方式,实际上是通过 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";
}
31、Exception 和 Error 有什么区别
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类
- Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked
Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。 - Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
32、Checked Exception 和 Unchecked Exception 有什么区别
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException…。
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
33、Throwable 类常用方法有哪些
- String getMessage(): 返回异常发生时的简要描述
- String toString(): 返回异常发生时的详细信息
- String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
- void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息
34、try-catch-finally 如何使用
- try块 : 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- catch块 : 用于处理 try 捕获到的异常。
- finally 块 : 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
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 返回值。
35、finally 中的代码一定会执行吗
不一定的!在某些情况下,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 块的代码也不会被执行:
- 程序所在的线程死亡。
- 关闭 CPU。
36、throw 和 throws 的区别
throw 作用在方法内,表示抛出具体异常,throws 作用在方法的声明上,表示方法抛出异常,由调用者来进行异常处理。
37、常见的异常类有哪些
- NullPointerException:空指针异常
- SQLException:数据库相关的异常
- IndexOutOfBoundsException:数组下角标越界异常
- FileNotFoundException:打开文件失败时抛出
- IOException:当发生某种IO异常时抛出
- ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出此异常
- NoSuchMethodException:无法找到某一方法时,抛出
- ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常
- NumberFormatException:当试图将字符串转换成数字时,失败了,抛出
- IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数
38、什么是泛型,有什么作用
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList persons = new ArrayList() 这行代码就指明了该 ArrayList 对象只能传入 Person 对象,如果传入其他类型的对象就会报错。
ArrayList<E> extends AbstractList<E>
并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。
39、泛型的使用方式有哪几种
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
- 泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
- 泛型接口
public interface Generator<T> {
public T method();
}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
- 泛型方法 :
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
注意: public static < E > void printArray( E[] inputArray ) 一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的
40、何谓反射
运行期间动态获取类中的信息(属性,方法,包的信息,注解等)以及动态调用对象的方法和属性的功能,称之为java语言的反射机制,通俗的理解,就是在运行期间对类的内容进行操作。
https://blog.csdn.net/yy139926/article/details/124831677
41、何谓注解
Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。注解本质是一个继承了Annotation 的特殊接口:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
JDK 提供了很多内置的注解(比如 @Override 、@Deprecated),同时,我们还可以自定义注解。
42、注解的解析方法有哪几种
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value 、@Component)都是通过反射来进行处理的。
43、什么是序列化,什么是反序列化
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
下面是序列化和反序列化常见应用场景:
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化。
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化。
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
44、序列化协议对应于 TCP/IP 4 层模型的哪一层
如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?
因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
45、如果有些字段不想进行序列化怎么办
对于不想进行序列化的变量,使用 transient 关键字修饰。transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 transient 还有几点注意:
- transient 只能修饰变量,不能修饰类和方法。
- transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。
- static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
46、常见序列化协议有哪些
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
47、为什么不推荐使用 JDK 自带的序列化
我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:
- 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
- 性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
- 存在安全问题 :序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
48、Java IO 流了解吗
IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
49、I/O 流为什么要分为字节流和字符流呢
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
- 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。
50、java 中都有哪些引用类型
1、强引用
Java中默认声明的就是强引用,比如:
Object obj = new Object();
obj = null;
只要强引用存在,垃圾回收器将永远不会回收被引用的对象。如果想被回收,可以将对象置为null
2、软引用
在内存足够的时候,软引用不会被回收,只有在内存不足时,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会跑出内存溢出异常。
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
3、弱引用
进行垃圾回收时,弱引用就会被回收。
4、虚引用(PhantomReference)
5、引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用、虚引用一起配合使用。当垃圾回收器准备回收一个对象时,如果发现它还有引用,就会在回收对象之前,把这个引用加入到引用队列中。程序可以通过判断引用队列中是否加入了引用,来判断被引用的对象是否将要被垃圾回收,这样可以在对象被回收之前采取一些必要的措施。
51、BIO、NIO 和 AIO 的区别
网络IO模型有BIO、NIO、AIO
- BIO:同步阻塞IO。
- NIO:同步非阻塞IO。
- AIO:异步非阻塞IO。
首先在网络编程中,客户端给服务端发送消息大约分为两个个步骤。
- 发起连接。
- 发送数据。
在BIO中每一个连接都需要分配一个线程来执行,假如A客户端连接了服务器,但是还没有发送消息,这个时候B客户端向服务器发送连接请求,这个时候服务器是没有办法处理B客户端的连接请求的。
因为一个线程处理了一个客户端的连接后就阻塞住,并等待处理该客户端发送过来的数据。处理完该客户端的数据后才能处理其他客户端的连接请求。
那你这个是只有一个线程的时候,那我弄多个线程不就好了,来一个请求连接我弄一个线程?那假如有一万个连接请求同时过来,那你开启一万个线程服务端不就崩了嘛!那我弄一个线程池呢,我最大线程数最多弄500呢?那假如有500线程只请求连接,并不发送数据呢,那你这个线程池不也一样废了吗。这500个请求连接上了还没有发送数据,那么线程池的500个线程就没办法去处理别的请求,这样照样废废了。
那咋办呢?
可以使用NIO同步非阻塞,这样就不需要很多线程,一个线程也能处理很多的请求连接和请求数据。NIO他是怎么实现一个线程处理多个连接请求和多个请求数据的呢?NIO会将获取的请求连接放入到一个数组中,然后再遍历这个数据查看这些连接有没有数据发送过来。
但是有个问题啊,如果B和C只连接了,但是一直没有发送数据,那每次还循环判断他俩有没有发送数据的请求是不是有点多余了,能不能在我知道B和C肯定发送了数据的情况下再去遍历他呢?可以引入Epoll,在JDK1.5开始引入了epoll通过事件响应来优化NIO,原理是客户端的每一次连接和每一次发送数据都看作是一个事件,每次发生事件会注册到服务端的一个集合中去,然后客户端只需要遍历这个集合就可以了。
那AIO有什么特点呢?
AIO是异步非阻塞,他对于客户端的连接请求和发送数据请求是用不同的线程来处理的,他是通过回调来通知服务端程序去启动线程处理,适用于长连接的场景。
52、当Java程序中用到的数值位数特别多时怎么办
当程序中用到的数值位数特别多时,为了解决这种问题,Java 7引入了一个新功能:程序员可以在数值中使用下画线,不管是整型数值,还是浮点型数值,都可以自由地使用下画线。通过使用下画线分隔,可以更直观地分辨数值常量中到底包含多少位。如double a = 3.14_15_92_65_36。
53、int 和 Integer 有什么区别
- int是基本数据类型,Integer是int的封装类型
- int可以没有初始值,Integer必须设置初始值
- int默认为0,Integer默认为null
54、Final、Finally、Finalize区别
final可以修饰类,变量,方法,修饰的类不能被继承,修饰的变量不能重新赋值,修饰的方法不能被重写。
finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。
finalize方法用于垃圾回收。finalize() Object 类中定义的方法,Java 中允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集 器在销毁对象时调用的,通过重写 finalize() 方法可以整理系统资源或者执行其他清理工作。
55、实例化对象有哪几种方式
- new
- 克隆的 clone() 方法
- 通过反射机制创建 newinstance()方法
- 序列化反序列化,将一个对象实例化后,进行序列化,再反序列化,也可以获得一个对象
56、byte类型127+1等于多少
byte的范围是-128~127。
字节长度为8位,最左边的是符号位,而127的二进制为01111111,所以执行+1操作时,01111111变为10000000。
大家知道,计算机中存储负数,存的是补码的兴衰。左边第一位为符号位。
那么负数的补码转换成十进制如下:
一个数如果为正,则它的原码、反码、补码相同;一个正数的补码,将其转化为十进制,可以直接转换。
已知一个负数的补码,将其转换为十进制数,步骤如下:
先对各位取反;将其转换为十进制数;加上负号,再减去1;
例如10000000,最高位是1,是负数,①对各位取反得01111111,转换为十进制就是127,加上负号得-127,再减去1得-128;
57、Java方法重写需要遵循什么原则?
方法的重写要遵循 “两同两小一大” 规则。
“两同” :方法名相同、形参列表相同。
“两小” :指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等。
“一大” :指的是子类方法的访问权限应比父类方法的访问权限更大或相等。尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
58、Java的instanceof有什么用处
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
boolean result = obj instanceof class
其中 obj 为一个对象,Class 表示一个类或者一个接口。
当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
59、创建一个Java对象时,执行顺序是怎样的?
- 父类静态代码块,静态代码块之间按代码顺序执行。
- 子类静态代码块,静态代码块之间按代码顺序执行。
- 父类实例代码块,实例代码块之间按代码顺序执行。
- 父类的构造函数。
- 子类实例代码块,实例代码块之间按代码顺序执行。
- 子类的构造函数。
60、使用实体类作为入参的优缺点
使用实体类作为入参的优点
- 实体类作为入参时,在属性方面和函数两方面都可以良好的体现面向对象和封装的概念。
JavaBean的属性方面:JavaBean的属性明确即使多个人同时使用同一个JavaBean,也可以避免本质上相同属性出现多个不同命名的情况。
JavaBean的函数方面:当属性需要特殊加工处理时,可以在实体类内封装函数处理需要加工的属性,体现了对特定逻辑的封装、包装,方便团队间理解和后期维护。 - 实体类与IBatis或MyBatis进行交互,作为返回结果时,通过配置属性与字段映射关系可以降低实体类与sql之间的耦合关系,当sql改动时修改映射关系就可以无需改动对应的实体类属性。
- 实体类的一些问题可以在代码编译期排除,而不需要等到运行时才发现错误,可以减少在运行时需要排查的问题点。
使用实体类作为入参的缺点
- 增加大量代码量,需要更多的时间需考虑对实体类属性与业务函数的实际封装。
- 会降低业务程序的开发进度。
- 当实体类属性需要增加时,改动地方要比使用Map的形式时要多一些,要增加实体类属性、get/set方法、封装的业务逻辑函数以及实体类与sql字段的映射关系等。
61、使用 Map 作为入参的优缺点
使用Map作为入参的优点
- 使用Map不需要定义属性以及初始化get、set方法和构造函数,可以简化这些操作部分。
- Map的灵活性要高于实体类,可以根据不同的封装内容,使用于不用的场景,而这样也就意味着Map的扩展性很强。
- Map可以直接作为IBatis、MyBatis的返回结果,省略了实体类作为返回结果时需要将返回字段与实体属性进行关系映射的步骤,可以简化操作。
使用Map作为入参的缺点
- 使用Map作为入参需要明确维护好每个key参数对应的使用场景及明确描述key的作用等,否则后期需要维护的时候这就是个让你头疼的坑。
- 团队开发时,如果多个人同时需要使用同一个Map时,会出现Map中出现多个不同的key,但是保存的值是相同的,这样会导致代码冗乱,多个业务之间衔接时不方便理解。
- 如果与IBatis、或MyBatis交互作为返回结果时,编译期无法确认参数是否有误,若参数有误需要到sql交互层进行确认。
62、Java基本类型的参数传递和引用类型的参数传递有啥区别?
- 当使用基本数据类型作为方法的形参时,在方法体中对形参的修改不会影响到实参的数值。
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]
63、Java中"hello"直接量和new String(“hello”)有什么区别呢?
当Java程序直接使用形如"hello"的字符串直接量时,JVM将会使用常量池来管理这些字符串,当使用new String(“hello”)时,JAVA虚拟机首先在字符串池中查找是否已经存在了值为“hello”的这么一个对象,它的判断依据是String类equals()方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用,如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。然后把常量池中的地址放到堆内存中,然后在栈中创建一个引用指向其堆内存块对象(此过程中可能会创建两个对象,也可能就一个)。
64、Java final修饰基本类型变量和引用类型变量的区别?
当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
65、Java中怎样的final变量可执行“宏替换”?
对一个final变量来说,不管它是类Field、实例Field,还是局部变量,只要该变量满足3个条件,这个final变量就不再是一个变量,而是相当于一个直接量。
- 使用final修饰符修饰。
- 在定义该final变量时指定了初始值。
- 该初始值可以在编译时就被确定下来。
final String s1 = "aaa"; // 可执行宏替换
final String s2 = s1 // 不可执行宏替换
final String s3 = getValue() // 不可执行宏替换
66、枚举类与普通类有什么区别?
- enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类。
- 如果省略了构造器的访问控制符,则默认使用private修饰,如果强制指定访问控制符,则只能指定private修饰符。
- 枚举类的每个实例必须在枚举类中显示的列出(,分隔 ;结尾) 列出的实例系统会自动添加public static final修饰,所有的枚举类都定义了一个values方法,该方法可以很方便的遍历所有的枚举值。
- 枚举类对象的属性不能更改,所以要用private final修饰
- 枚举类对象要在构造器中被赋值。
67、Java枚举类不是用final修饰了吗?怎么还能派生子类呢?
枚举类,如果不包含抽象方法,那么会默认采用final修饰类,而如果包含抽象方法,那么由于抽象类是不能被final修饰(抽象类如果不实现就没什么意义,用final修饰的话会导致其不能被实现),此时类是被abstract修饰类,所以是可以派生子类的。
68、使用Java的IO流执行输出时,为啥要关闭输出流?
使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,可能还可以将输出流缓冲区中的数据flush到物理节点里(因为在执行close()方法中自动执行flush()方法)。Java的很多输出流默认都提供了缓冲功能,其实我们没有必要刻意去记忆哪些流有缓冲功能、哪些流没有,只要正常关闭所有的输出流即可保证程序正常。
69、Java如何使用Proxy和InvocationHandler创建动态代理?
@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());
}
70、举例说明什么情况下会更倾向于使用抽象类而不是接口
接口和抽象类都遵循面向接口而不是实现编码设计原则,它可以增加代码的灵活性,可以适应不断变化的需求。下面有几个点可以帮助你回答这个问题:在 Java 中,你只能继承一个类,但可以实现多个接口。所以一旦你继承了一个类,你就失去了继承其他类的机会了。
接口通常被用来表示附属描述或行为如: Runnable 、 Clonable 、 Serializable 等等,因此当你使用抽象类来表示行为时,你的类就不能同时是 Runnable 和 Clonable( 注:这里的意思是指如果把 Runnable 等实现为抽象类的情况 ) ,因为在 Java 中你不能继承两个类,但当你使用接口时,你的类就可以同时拥有多个不同的行为。
在一些对时间要求比较高的应用中,倾向于使用抽象类,它会比接口稍快一点。如果希望把一系列行为都规范在类继承层次内,并且可以更好地在同一个地方进行编码,那么抽象类是一个更好的选择。有时,接口和抽象类可以一起使用,接口中定义函数,而在抽象类中定义默认的实现。