Java基础-String、Object、异常、反射、注解、I/O
String
String 有哪些常用方法?
length()
返回字符串的长度。charAt(int index)
返回指定位置的字符。substring(int beginIndex, int endIndex)
返回字符串的一个子串,从beginIndex
到endIndex-1
。contains
(CharSequence s)
检查字符串是否包含指定的字符序列。equals(Object anotherObject)
比较两个字符串的内容是否相等。indexOf(int ch)
和indexOf(String str)
返回指定字符或字符串首次出现的位置。replace(char oldChar, char newChar)
和replace(CharSequence target, CharSequence replacement)
替换字符串中的字符或字符序列。trim()
去除字符串两端的空白字符。split(String regex)
根据给定正则表达式的匹配拆分此字符串。
String、StringBuffer和StringBuilder的区别
从可变性和是否线程安全、执行效率三方面回答
- String 不可变(创建String对象后,该对象中的字符串是不可改变的,直到这个对象被销毁。不能在原地址上修改对象,每次修改都会生成新的对象返回给引用变量),StringBuilder 和 StringBuffer 是可变的(继承自AbstractStringBuilder, 使用字符数组保存字符串,是可变类)
- String 由于是不可变的,所以线程安全。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的
- 在执行效率方面,StringBuilder最高,StringBuffer次之,String最低
对于三者使用的总结
- String:适用于字符串内容不会改变的场景,比如说作为 HashMap 的 key。
- StringBuilder:适用于单线程环境下需要频繁修改字符串内容的场景,比如在循环中拼接或修改字符串,是 String 的完美替代品。
- StringBuffer:现在已经不怎么用了,因为一般不会在多线程场景下去频繁的修改字符串内容。
String为什么不可变?
- 保存字符串的数组被 final修饰且为私有的,并且string 类没有提供修改这个字符串的方法。
private final char value[];
像 concat 这些看似修改字符串的操作,实际上都是返回一个新创建的字符串对象,而原始字符串对象保持不变。
- string 类被 final修饰导致其不能被继承,进而避免了子类破坏 string 不可变。
public final class String
String不可变的好处 作为参数、缓存和重用、作为键值对集合的key
- 不可变性使得 String 对象在使用中更加安全。因为字符串经常用作参数传递给其他 Java 方法,例如网络连接、打开文件等。
- 不可变的对象因为状态不会改变,所以更容易进行缓存和重用。字符串常量池的出现正是基于这个原因。
- 因为 String 的内容不会改变,所以它的哈希值也就固定不变。这使得 String 对象特别适合作为 HashMap 或 HashSet 等集合的键,因为计算哈希值只需要进行一次,提高了哈希表操作的效率。
字符串拼接用"+”还是StringBuilder?
通过加号拼接字符串时会创建多个 String 对象是不准确的。因为加号拼接在编译期还会创建一个 StringBuilder 对象,最终调用 toString()
方法的时候再返回一个新的 String 对象。
在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷 : 编译器不会创建单个 stringBuilder 以复用,会导致创建过多的stringBuilder 对象。
如果直接使用 stringBuilder 对象进行字符串拼接的话,就不会存在这个问题
因此对于少量简单的字符串拼接,使用"+"操作符是方便的。而对于大量或循环拼接的情况,使用StringBuilder类可以提供更好的性能和内存效率。
字符串常量池的作用了解吗?
字符串常量池是JVM为了提升性能和减少内存消耗,针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String s1=new String("abc");这句话创建了几个字符串对象?
会创建1或2个字符串对象。
字符串常量池中如果之前已经有一个,则不再创建新的,直接引用;如果没有,则创建一个。
堆中肯定有一个,因为只要使用了 new 关键字,肯定会在堆中创建一个。
intern方法有什么作用?
string.intern()是一个 native (本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
在编程语言中,字面量(Literal)是指在源代码中直接指定的固定值。字面量是编程语言中表示数据的一种方式,它们通常是编译时常量,即在程序编译时它们的值就已经确定。
String s1 = "Java"; // 直接返回3
String s2 = s1.intern(); // 直接返回s1指向的字符串对象
String s3 = new String("Java");//使用 new 创建了新对象 堆
String s4 = s3.intern(); // 直接返回s1指向的字符串对象
System.out.println(s1 == s2); // true,s1和s2指向同一个字符串对象
System.out.println(s3 == s4); // false,s3和s4指向不同的字符串对象
System.out.println(s1 == s4); // true,s1和s4指向同一个字符串对象
String 类型的变量和常量做”+”运算时发生了什么?
对于编译期可以确定值的字符串,也就是常量字符串,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
对于 String str3 ="str"+"ing"; 编译器会给你优化成string str3 ="string";
String s1 = "str";
String s2 = "ing";
String s3 ="str"+"ing" ; //放入常量池
String s4 = s1+s2;//新建对象 堆
System.out.println(s3 == s4); // false ==比较引用地址是否相等
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型(byte、boolean 、int、char 、short、float、long、double)以及字符串常量。
- final 修饰的基本数据类型和字符串变量
- 字符串("str"+"ing")通过“+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算 (<<、>>、>>>)
- 引用的值在程序编译期是无法确定的,编译器无法对其进行优化
- 被final 关键字修改之后的 string 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。如果,编译器在运行时才能知道其确切值的话,就无法对其优化
s1
、s2
和s3
都是使用字符串字面量创建的,它们会被编译器放入字符串常量池中。字符串常量池是Java运行时环境中的一个特殊区域,用于存储字符串字面量,以便实现字符串的重用。
而s4
实际上是通过stringBuilder,调用append() 方法实现的,拼接完成之后调用 tostring() 得到一个String 对象,放在堆中
String 怎么转成 Integer 的?原理?
String 转成 Integer,主要有两个方法:
- Integer.parseInt(String s)
- Integer.valueOf(String s)
不管哪一种,最终还是会调用 Integer 类内中的parseInt(String s, int radix)
方法。
Object
常见的Object方法
反射
- class getClass():返回一个对象运行时的实例类
对象比较
- int hashcode():返回该对象的hash码值
- boolean equals(object)比较两对象地址是否相等
对象拷贝
- object clone():创建并返回当前对象的拷贝
线程调度
- void notify():唤醒等待在该对象的监视器上的一个线程
- void notifyAll():唤醒等待在该对象的监视器上的全部线程
- void wait():在其他线程调用此对象的 notify() 方法或 notifyAll()方法前,导致当前线程等待
- void wait(long timeout)
- void wait(long timeoutMillis, int nanos)
垃圾回收
- void finalize():当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法
对象转字符串
- string tostring():返回该对象的字符串表示
默认实现返回类名@哈希码的十六进制表示,但通常会被重写以返回更有意义的信息。
异常
java的异常体系
Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
- Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
- Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序(比如JVM自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类。
Exception又分为可检查(checked)异常(IO)和不检查(unchecked)异常(NPE)
可检查异常在源代码里必须显式地进行捕获处理(编译时异常),这是编译期检查的一部分。 IOException
不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
异常处理
- 遇到异常不进行具体处理,而是继续抛给调用者 (throw,throws)
抛出异常有三种形式,一是 throw,一个 throws,还有一种系统自动抛异常。
throws 用在方法上,后面跟的是异常类,可以跟多个;而 throw 用在方法内,后面跟的是异常对象。
- try catch 捕获异常
在 catch 语句块中捕获发生的异常,并进行处理。
- finally先执行,try后执行
经典异常处理代码题
public class TryDemo {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.print("3");
}
}
}
无论前面的代码是否发生异常,finally块总是会执行。在这个例子中,finally块包含一条System.out.print("3");语句,意味着在方法结束前,会在控制台打印出3。
因为finally块确保了它包含的System.out.print("3");会执行并打印3,随后test()方法返回try块中的值1,最终结果就是31。
输出 31
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
int i = 0;
try {
i = 2;
return i;
} finally {
i = 3;
}
}
}
执行结果:2。
大家可能会以为结果应该是 3,因为在 return 前会执行 finally,而 i 在 finally 中被修改为 3 了,那最终返回 i 不是应该为 3 吗?
但其实,在执行 finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,所以即使 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。
反射
Java的反射机制和原理
Java 的反射是指在程序的运行状态中,可以构造任意一个类的实例,访问一个实例的结构,访问实例的成员变量和方法机制,动态加载类,提高代码灵活度
优点:运行期类型的判断,动态加载类,提高代码灵活度
缺点:性能比直接的 java 代码要慢,程序复杂度比较高
反射功能主要通过 java.lang.Class
类及 java.lang.reflect
包中的类如 Method, Field, Constructor 等来实现。
反射的应用场景?
- Spring 框架就大量使用了反射来动态加载和管理 Bean。
- Java 的动态代理(Dynamic Proxy)机制就使用了反射来创建代理类。代理类可以在运行时动态处理方法调用,这在实现 AOP 和拦截器时非常有用。
- JUnit 和 TestNG 等测试框架使用反射机制来发现和执行测试方法。反射允许框架扫描类,查找带有特定注解(如
@Test
)的方法,并在运行时调用它们。
反射的原理是什么?
我们都知道 Java 程序的执行分为编译和运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类型相关的所有信息加载进方法区,反射就是去获取这些信息,然后进行各种操作
注解
注解是什么?解析方法有几种
注解是Java5开始引入的新特性,Java 注解本质上是一个标记,可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。有了标记之后,我们就可以在编译或者运行阶段去识别这些标记,然后搞一些事情,这就是注解的用处。
例如我们常见的 AOP,使用注解作为切点就是运行期注解的应用;比如 lombok,就是注解在编译期的运行。
注解生命周期有三大类,分别是:
- RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
- RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息
- RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描:编译器在编译Java代码的时候扫描对应的注解并处理,比如某个方法使用 @override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的@Autowired、@Value、@Component)都是通过反射来进行处理的。
I/O
Java 中 IO 流分为几种?
Java IO 流的划分可以根据多个维度进行,包括数据流的方向(输入或输出)、处理的数据单位(字节或字符)、流的功能以及流是否支持随机访问等。
按照数据流方向如何划分?
- 输入流(Input Stream):从源(如文件、网络等)读取数据到程序。
- 输出流(Output Stream):将数据从程序写出到目的地(如文件、网络、控制台等)。
按处理数据单位如何划分?
- 字节流(Byte Streams):以字节为单位读写数据,主要用于处理二进制数据,如音频、图像文件等。
- 字符流(Character Streams):以字符为单位读写数据,主要用于处理文本数据。
按功能如何划分?
- 节点流(Node Streams):直接与数据源或目的地相连,如 FileInputStream、FileOutputStream。
- 处理流(Processing Streams):对一个已存在的流进行包装,如缓冲流 BufferedInputStream、BufferedOutputStream。
- 管道流(Piped Streams):用于线程之间的数据传输,如 PipedInputStream、PipedOutputStream。
IO 流用到了什么设计模式?
其实,Java 的 IO 流体系还用到了一个设计模式——装饰器模式。
Java 缓冲区溢出,如何预防
Java 缓冲区溢出主要是由于向缓冲区写入的数据超过其能够存储的数据量。可以采用这些措施来避免:
①、合理设置缓冲区大小:在创建缓冲区时,应根据实际需求合理设置缓冲区的大小,避免创建过大或过小的缓冲区。
②、控制写入数据量:在向缓冲区写入数据时,应该控制写入的数据量,确保不会超过缓冲区的容量。Java 的 ByteBuffer 类提供了remaining()
方法,可以获取缓冲区中剩余的可写入数据量。
既然有了字节流,为什么还要有字符流?
其实字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还比较耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。
所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
文本存储是字节流还是字符流,视频文件呢?
在计算机中,文本和视频都是按照字节存储的,只是如果是文本文件的话,我们可以通过字符流的形式去读取,这样更方便的我们进行直接处理。
比如说我们需要在一个大文本文件中查找某个字符串,可以直接通过字符流来读取判断。
处理视频文件时,通常使用字节流(如 Java 中的FileInputStream
、FileOutputStream
)来读取或写入数据,并且会尽量使用缓冲流(如BufferedInputStream
、BufferedOutputStream
)来提高读写效率。
无论是文本文件还是视频文件,它们在物理存储层面都是以字节流的形式存在。区别在于,我们如何通过 Java 代码来解释和处理这些字节流:作为编码后的字符还是作为二进制数据。
BIO/NIO/AIO的区别 (连接(BIO)-->请求(NIO)-->有效请求(AIO))
简单说一下 BIO?
BIO,也就是传统的 IO,基于字节流或字符流(如 FileInputStream、BufferedReader 等)进行文件读写,基于 Socket 和 ServerSocket 进行网络通信。
对于每个连接,都需要创建一个独立的线程来处理读写操作。如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在JDK1.4以前是唯一的IO
简单说下 NIO?
NIO,JDK 1.4 时引入,放在 java.nio 包下,提供了 Channel、Buffer、Selector 等新的抽象,基于 RandomAccessFile、FileChannel、ByteBuffer 进行文件读写,基于 SocketChannel 和 ServerSocketChannel 进行网络通信。
NIO 的魅力主要体现在网络编程中,服务器实现模式为一个请求一个线程,服务器可以用一个线程处理多个客户端连接,通过 Selector 监听多个 Channel 来实现多路复用,极大地提高了网络编程的性能,
NIO (NEW IO)同步非阻塞IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器Selector上,多路复用器轮询到连接有IO请求(可读、可写、连接就绪、接收连接)时才启动一个线程进行处理。(可以有多个线程)
缓冲区 Buffer 也能极大提升一次 IO 操作的效率。
简单说下 AIO?
AIO 是 Java 7 引入的,放在 java.nio.channels 包下,提供了 AsynchronousFileChannel、AsynchronousSocketChannel 等异步 Channel。
AIO(Asynchronous IO) 异步非阻塞IO,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。
AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂
自己整理,借鉴很多博主 感谢他们
原文地址:https://blog.csdn.net/m0_50846237/article/details/140567724
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!