字符型常量和字符串常量区别
字符型常量 (char)值用单引号引起,值可以转换为整型 与ASCII值相照应,可参加运算。java字符常量占两个字节。
字符串常量(String)值用双引号引起,是引用类型,储存的是该字符串在内存中的位置。
补充:
字符串储存位置
如果是直接赋值 如:(String str=”twm”), 引用指向的地址(字符串本身)存在方法区的常量池中,(如果该字符在常量池不存在,会直接在常量池生成一个)
如果是 new String。创建时在堆中创建字符串对象,其引用指向的是字符串在堆中的地址,当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护)。
常量字符串的“+”操作:
例如:String str=”JA”+”VA”,
在编译阶段会直接合成一个字符串 String str=”JAVA”。然后去查找常量池中是否存在”JAVA”,进而进行引用或创建。
对于final字段,编译期直接进行了常量替换(而对于非final字段则是在运行期进行赋值处理的)。
final String str1=”ja”;
final String str2=”va”;
String str3=str1+str2;
在编译时,直接替换成了String str3=”ja”+”va”。
常量字符串和变量拼接时
如:
final String baseStr = “JAVA“ ;
String str3=baseStr + “01”;
会调用StringBuilder.append()在堆上创建新的对象。
JDK 7 以后 intern 方法的改动
JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,
区别在于,**如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。**简单的说,就是往常量池放的东西变了原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。
continue、break、return区别
continue 跳出当次循环。
break 跳出整个循环体。
return 跳出方法。
hashCode() 与 equals()的区别
为什么要有 hashCode?
例如: HashSet 在放入对象时,会先计算对象 hashCode值 判断对象加入的位置,同时和已加入对象的hashCode值相比较,如果有相等的HashCode,会在调用equals方法检查 hashCode相等的对象是否相同,如果相同,HashSet不会让其加入,如果不同就会重新散列到其他位置。可以大大减少调用equals的次数。
hashCode和equals都是用来比较两个对象是否相等的。
那为什么 JDK 还要同时提供这两个方法呢?
因为一些容器,有了hashCode()之后,判断元素是否在对应容器中的效率更高 (HashMap,HashSet)
为什么只提供hashCode方法呢?
因为hashCode方法相等并不代表两个对象就相等。hashCode()所使用的hash算法可能会有对个对象传回相同的hash值。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象hashCode值必须是相等的,如果不相等,在使用HashSet等容器时,可能会造成equals相等但hash值不等,由于先调用的是hashCode()会导致我们预期的两个相等的元素同时加入到容器中。
成员变量 与 局部变量的区别
-
**从语法形式:**成员变量属于类,而局部变量是代码块或方法中定义的变量或者是方法的参数。成员变量可以用访问修饰符修饰,局部变量不行。但是局部和成员变量都能用final修饰。
-
储存方式:成员变量如果没有用static修饰符修饰,则这个成员变量是属于实例的,而对象存于堆内存中,局部变量则储存与栈内存中。如果用static修饰 ,在JDK8之前是存放在方法区,但JDK8之后取消了“永久代”,取而代之的是 “元空间” ,永久代的数据进行了迁移,静态变量迁移到了堆中。(他的引用是存在方法区的)
-
生存时间 :成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
-
**默认值:**成员变量有默认值,(如果用final修饰则没有),局部变量则没有。
深拷贝 和 浅拷贝
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
[外链图片转存中…(img-W4FhWou0-1645969744247)]
String
String、StringBuffer、StringBuilder 的区别?String 为什么是不可变的?
String不可变的原因
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
-
保存字符串的数组被final修饰且为私有额度,并且String没有提供/暴露修改这个字符串的方法。
-
String 类被 final 修饰导致其不能被继承,进而避免子类破坏String
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。
StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。
StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
字符串拼接用“+” 还是 StringBuilder?
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的元素符
对象引用和“+”的字符串拼接方式,实际上是通过 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
对象进行字符串拼接的话,就不会存在这个问题了。
字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa==bb);// true
JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区。JDK1.7 的时候,字符串常量池被从方法区拿到了堆中。
泛型
Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?
Java 泛型(generics) 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
Java 的泛型是伪泛型,这是因为 Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。
List<Integer> list = new ArrayList<>();
list.add(12);
//这里直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加是可以的
//这就说明在运行期间所有的泛型信息都会被擦掉
add.invoke(list, "kl");
System.out.println(list);
泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。
1.泛型类:
//此处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);
2.泛型接口 :
public interface Generator<T> { public T method();}
实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{ @Override public T method() { return null; }}
实现泛型接口,指定类型:
class GeneratorImpl implements Generator<String>{ @Override public String method() { return "hello"; }}
3.泛型方法 :
public static <E> void printArray(E[] inputArray) { for (E element : inputArray) { System.out.printf("%s ", element); } System.out.println();}
使用:
// 创建不同类型数组: Integer, Double 和 CharacterInteger[] intArray = { 1, 2, 3 };String[] stringArray = { "Hello", "World" };printArray(intArray);printArray(stringArray);
常用的通配符有哪些?
常用的通配符为: T,E,K,V,?
- ? 表示不确定的 Java 类型
- T (type) 表示具体的一个 Java 类型
- K V (key value) 分别代表 Java 键值中的 Key Value
- E (element) 代表 Element
反射
如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射机制优缺点
- 优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- 缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的
反射的应用场景
像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。
但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method
来调用指定的方法.
public class DebugInvocationHandler implements InvocationHandler { /** * 代理类中的真实对象 */ private final Object target; public DebugInvocationHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { System.out.println("before method " + method.getName()); Object result = method.invoke(target, args); System.out.println("after method " + method.getName()); return result; }}
另外,像 Java 中的一大利器 注解 的实现也用到了反射。
为什么你使用 Spring 的时候 ,一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value
注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
JDK 提供了很多内置的注解(比如 @Override
、@Deprecated
),同时,我们还可以自定义注解。
异常
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。Error
:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获 。例如Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception 即受检查异常,Java 代码在编译过程中,如果受检查异常没有被 catch
/throw
处理的话,就没办法通过编译 。
比如下面这段 IO 操作的代码:
除了RuntimeException
及其子类以外,其他的Exception
类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException
、SQLException
…。
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException
及其子类都统称为非受检查异常,例如:NullPointerException
、NumberFormatException
(字符串转换为数字)、ArrayIndexOutOfBoundsException
(数组越界)、ClassCastException
(类型转换错误)、ArithmeticException
(算术错误)等。
Throwable 类常用方法有哪些?
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
try-catch-finally 如何使用?
try
块: 用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。catch
块: 用于处理 try 捕获到的异常。finally
块: 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句不会被执行。
finally 中的代码一定会执行吗?
不一定的!在某些情况下,finally 中的代码不会被执行。
就比如说 finally
之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
另外,在以下 2 种特殊情况下,finally
块的代码也不会被执行:
- 程序所在的线程死亡。
- 关闭 CPU。
如何使用 try-with-resources
代替try-catch-finally
?
-
适用范围(资源的定义): 任何实现
java.lang.AutoCloseable
或者java.io.Closeable
的对象 -
关闭资源和 finally 块的执行顺序: 在
try-with-resources
语句中,任何 catch 或 finally 块在声明的资源关闭后运行
面对必须要关闭的资源,我们总是应该优先使用
try-with-resources
而不是try-finally
。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources
语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally
则几乎做不到这点。
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) { int b; while ((b = bin.read()) != -1) { bout.write(b); } } catch (IOException e) { e.printStackTrace(); }
什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
关于 transient
还有几点注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
获取用键盘输入常用的两种方法
方法 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();
Java 中 IO 流分为几种?
-
按照流的方向分:输入流和输出流
-
按照操作单元分:可分为字节流和字符流
-
按照流的角色分可分为节点流和处理流
-
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
-
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
既然有了字节流,为什么还要有字符流?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
为什么 Java 中只有值传递?
为什么说 Java 只有值传递呢? 不需要太多废话,我通过 3 个例子来给大家证明。
开始之前,我们先来搞懂下面这两个概念:
- 形参&实参
- 值传递&引用传递
形参&实参
方法的定义可能会用到 参数(有参的方法),参数在程序语言中分为:
- 实参(实际参数) :用于传递给函数/方法的参数,必须有确定的值。
- 形参(形式参数) :用于定义函数/方法,接收实参,不需要有确定的值。
String hello = "Hello!";// hello 为实参sayHello(hello);// str 为形参void sayHello(String str) { System.out.println(str);}
值传递&引用传递
程序设计语言将实参传递给方法(或函数)的方式分为两种:
- 值传递 :方法接收的是实参值的拷贝,会创建副本。
- 引用传递 :方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递。
案例1:传递基本类型参数
public static void main(String[] args) { int num1 = 10; int num2 = 20; swap(num1, num2); System.out.println("num1 = " + num1); System.out.println("num2 = " + num2);}public static void swap(int a, int b) { int temp = a; a = b; b = temp; System.out.println("a = " + a); System.out.println("b = " + b);}
输出:
a = 20b = 10num1 = 10num2 = 20
解析:
在 swap()
方法中,a
、b
的值进行交换,并不会影响到 num1
、num2
。因为,a
、b
的值,只是从 num1
、num2
的复制过来的。也就是说,a、b 相当于 num1
、num2
的副本,副本的内容无论怎么修改,都不会影响到原件本身。
通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看案例2。
案例2:传递引用类型参数1
代码:
public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; System.out.println(arr[0]); change(arr); System.out.println(arr[0]); } public static void change(int[] array) { // 将数组的第一个元素变为0 array[0] = 0; }
输出:
01
解析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CqVscJ5j-1645969744251)(https://snailclimb.gitee.io/javaguide/docs/java/basis/images/java-value-passing-02.png)]
案例3 :传递引用类型参数2
public class Person { private String name; // 省略构造函数、Getter&Setter方法}public static void main(String[] args) { Person xiaoZhang = new Person("小张"); Person xiaoLi = new Person("小李"); swap(xiaoZhang, xiaoLi); System.out.println("xiaoZhang:" + xiaoZhang.getName()); System.out.println("xiaoLi:" + xiaoLi.getName());}public static void swap(Person person1, Person person2) { Person temp = person1; person1 = person2; person2 = temp; System.out.println("person1:" + person1.getName()); System.out.println("person2:" + person2.getName());}
输出:
person1:小李person2:小张xiaoZhang:小张xiaoLi:小李
解析:
怎么回事???两个引用类型的形参互换并没有影响实参啊!
swap
方法的参数 person1
和 person2
只是拷贝的实参 xiaoZhang
和 xiaoLi
的地址。因此, person1
和 person2
的互换只是拷贝的两个地址的互换罢了,并不会影响到实参 xiaoZhang
和 xiaoLi
。
总结
Java 中将实参传递给方法(或函数)的方式是 值传递 :
- 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。
反射实战
获取 Class 对象的四种方式
1.知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
2.通过 Class.forName()
传入类的路径获取:
Class alunbarClass1 = Class.forName("cn.javaguide,TagrgetObject")
3.通过对象实例instance.getClass()
获取:
TargetObject o = new TargetObject();Class alunbarClass2 = o.getClass();
4.通过类加载器xxxClassLoader.loadClass()
传入类路径获取:
ClassLoader.loadClass("cn.javaguide.TargetObject");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态块和静态对象不会得到执行
反射的一些基本操作
1.创建一个我们要使用反射操作的类 TargetObject
。
package cn.javaguide;public class TargetObject { private String value; public TargetObject() { value = "JavaGuide"; } public void publicMethod(String s) { System.out.println("I love " + s); } private void privateMethod() { System.out.println("value is " + value); }}
反射的一些基本操作
1.创建一个我们要使用反射操作的类 TargetObject
。
package cn.javaguide;public class TargetObject { private String value; public TargetObject() { value = "JavaGuide"; } public void publicMethod(String s) { System.out.println("I love " + s); } private void privateMethod() { System.out.println("value is " + value); }}