java基础详解
java基础详解
概念
基础框架建立
- 对象实例在堆内存中
java中堆栈的区别
栈:主要存储局部变量和方法的调用信息(如返回地址参数等)。在方法执行期间,局部变量被创建在栈上,并在方法结束时销毁。
堆:用于存储对象实例和数组。每次用new关键字创建对象时,JVM都会在堆上为该对象分配内存空间
面向对象基础
构造方法的特点
- 名称与类名相同
- 没有返回值
- 自动执行
- 不能被重写(override),可以被重载
面向对象的三大特征
1.封装:
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息
2.继承:
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
子类可以拥有自己属性和方法,即子类可以对父类进行扩展
3.多态
表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例
String
String、StringBuffer、StringBuilder的区别
安全性:
String不可变,也就可以理解为常量,线程安全。
StringBuilder 与 StringBuffer都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder中也是使用字符数组保存字符串
AbstractStringBuilder定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf。而StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,也是线程安全的。
性能
每次对String进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。相同情况下使用
StringBuilder
相比使用StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
异常
Exception和Error有什么区别
Exception :程序本身可以处理的异常,可以通过
catch
来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)Error:Error 属于程序无法处理的错误 ,我们没办法通过
catch
来进行捕获不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等
Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception 即受检查异常,java代码在编译过程中,如果受检查异常没有被catch或throws关键字处理的话,就没办法通过编译。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException…。
Unchecked Exception 即不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)
序列化和反序列化
- 序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
- 反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
常见应用场景:
对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化
将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化
将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化
序列化协议对应于 TCP/IP 4 层模型的哪一层?
我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于TCP/IP 协议应用层的一部分。
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
关于 transient 还有几点注意:
- transient 只能修饰变量,不能修饰类和方法。
- transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰
int
类型,那么反序列后结果就是0
。 - static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化
I/O
IO 即 Input/Output ,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
集合
Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素;另一个是 Map
接口,主要用于存放键值对。
说说Java中集合
List是有序的Collection,使用此接口能够精确的控制每个元素的插入位置,用户能根据索引访问List中元素。常用的实现List的类有LinkedList,ArrayList,Vector,Stack。
- ArrayList是容量可变的非线程安全列表,其底层使用数组实现。当几何扩容时,回创建更大的数组,并把原数组复制到新数组。ArrayList支持对元素的快速随机访问,但插入与删除速度很慢
- LinkedList本质是一个双向链表,与ArrayList相比,其插入和删除的速度更快,但随机访问速度更慢。
Set不允许存在重复的元素,与List不同,set中的元素是无序的。常用的实现有HashSet、LinkedHashSet和TreeSet。
- HashSet通过HashMap实现,HashMap的key即HashSet的元素,所有Key都是相同的Value,一个名为PRESENT的object类型常量。使用Key保证元素的唯一性,但不保证有序性。线程不安全。
- LinkedHashSet继承至HashSet,通过LinkedHashMap实现,使用双向链表维护元素插入顺序。
- TreeSet通过TreeMap实现的,添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。
Map是一个键值对集合,存储键、值和之间的映射。key无序,唯一;value不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。主要实现有TreeMap、HashMap、HashTable、LinkedHashMap、ConcurrentHashMap
- HashMap:由数组+链表组成。数组是HashMap的主体,链表则是为了解决哈希冲突而存在的。在JDK1.8之后,在解决哈希冲突时,当链表长度大于8时,将链表转化为红黑树,以减少搜索时间。
- LinkedHashMap:LinkedHashMap继承自HashMap,在HashMap的基础上增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
- HashTable:线程安全,内部大部分方法都使用了synchronized关键字修饰
- ConcurrentHashMap:是Hashtable的替代方案,线程安全的同时通过分段锁机制提高了并发性能。
说说 List, Set, Queue, Map 四者的区别?
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),”x” 代表 key,”y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
list可以一边遍历一边修改元素吗
在 Java 中,List
在遍历过程中是否可以修改元素取决于遍历方式和具体的List
实现类:
使用普通的for循环遍历:可以在遍历过程中修改元素,只要修改的索引不超出List的范围即可
使用foreach循环遍历:不建议在foreach循环中修改元素,因为这可能会导致意外结果或ConcurrentModificationExcetion异常。在foreach循环中修改元素可能会破坏迭代器的内部状态,因为foreach底层是基于迭代器实现的。
使用迭代器遍历:可以使用迭代器的remove和set方法来删除修改元素的值,而不是直接通过List的set方法,否则也可能抛出ConcurrentModificationExcetion异常。
1
2
3
4
5
6
7
8
9
10
11List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integter> iterator = list.iterator();
while(iterator.hasNext()){
Integer num = iterator.next();
if(num == 2){
iterator.set(4);
}
}
list如何快速删除某个指定下标的元素?
ArrayList提供了remove(int index)方法来删除指定下标的元素,该方法在删除元素后,会将后续元素向前移动,以填补被删除元素的位置。如果删除的是队尾的元素,时间复杂度为O(1),其他为O(n)。
ArrayList是线程安全的吗?把ArrayList变成线程安全由哪些方法?
不是线程安全的,ArrayList变成线程安全的方法有:
使用Collections类的synchronized方法将ArrayList包装成线程安全的List:
1
List<String> list = Collections.synchronized(arrayList);
使用CopyOnWriteArrayList或Vector类代替ArrayList,它是一个线程安全的List实现。
如何对map进行快速遍历
使用for-each循环和entrySet方法:这是一种较为常见和简洁的遍历方式,它可以同时获取Map中的键和值
1
2
3for(Map.Entry<String,Integer> entry :map.entrySet()){
System.out.println("key:"+ entry.getKey()+",Value:" + entry.getValue());
}使用for-each和keySet()方法:如果只需要遍历Map中的键,可以使用keySet()方法。
使用迭代器:通过获取Map的entrySet()或keySet()的迭代器,也可以实现对Map的遍历,这种方式需要删除元素等操作时比较有用。
使用Lambda表达式和forEach()方法:
1
map.forEach(key,value)->System.out.println("key:"+key+",Value:"+value);
使用Stream API:可以将MAp转换成流,然后进行各种操作。
1
map.entrySet().stream().forEach( entyr -> System.out.println("key:"+entry.getKey()+";Value"+entry.getValue()));
List list = new ArrayList<>();
为什么新建一个ArrayList的时候通常赋值给List接口类型的变量?
- 是一种面向接口编程的实践,强调依赖于接口或抽象类,而不是具体实现。
- 将List和具体实现ArrayList解耦,降低耦合度
- 增强代码的可读性和可维护性
- 可以在不修改代码的情况下轻松切换 List的具体实现。
- 方便使用多态,将任何实现了List接口的对象赋值给List类型的变量
ArrayList
ArrayList如何实现自动扩容
数组的初始容量是 10
(默认值),当数组容量不足时,会自动扩容:创建一个新的数组,是原数组长度的1.5倍,然后使用Array.copyOf()方法把原数组中的数据复制到新数组中,扩容完成后,再把要添加的数据加入新数组中。
谈谈ArrayList、Vector、LinkedList的存储性能及特性
ArrayList和Vector的底层都采用数组来存储数据,而且都根据索引来获取数据,每次扩容都需要易懂数组中的元素,这样设计使得获取数据快而插入删除数据慢。另外,Vector中的方法都使用了synchronized关键字修饰,因此Vector中的数据操作都是线程安全的,但性能会比ArrayList更差。
而LinkedList底层是采用双向链表来存储数据的。
HashMap
HashMap主要用来处理键值对数据,它是基于Hash表对Map接口的实现类,其特点是访问数据快,不是按顺序来遍历的。提供所有可选的映射操作,但不能保证映射顺序不变,并且允许使用空值和空键。
HashMap也并不是线程安全的,当存在多个线程同时写入时,可能会导致数据不一致的情况。
HashMap如何解决Hash冲突的
Hash算法就是把任意长度的输入,通过散列算法,变成固定长度的输出,这个输出结果是散列值;Hash表有叫做散列表,它通过key直接访问内存存储的数据,在具体实现上,通过hash()函数把key映射到表的某个位置,来获取这个位置的数据,从而加快查找速度
由于Hash()算法被计算的值是无限的,而计算后的结果范围有限,所以总会存在不同的数据经过计算后得到的值相同,这就是Hash冲突。
HashMap通过连链式寻址法+红黑树的方式来解决Hash冲突,其中单向链表法就是把存在Hash冲突的key,以单项链表的方式存储。红黑树是为了解决Hash表链表过长导致时间复杂度增加的问题,当链表长度大于8并且Hash表的容量大于64的时候,再向链表中添加数据就会触发转化。
HashMap什么时候扩容,如何自动扩容
HashMap的默认容量大小是16,在实际开发中,我们需要存储的数据量往往是大于默认容量大小的,所以就需要扩容。基本的 扩容逻辑就是新建一个更长是数据,然后把原来的数组里的数据复制到新的数组中。HashMap触发扩容的临界值是负载因子乘以容量大小,默认的负载因子是0.75,扩容后的大小是原来的两倍。由于扩容机制的存在,我们在实际应用时,最好在初始化时明确指定集合的大小,从而避免频繁扩容导致的性能消耗。
线程安全
是相对于多线程或者并发的情况而言的。如果是单线程操作就无所谓线程安全。
指的是在多线程情况下,访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,在不做任何干预的情况下,调用这个对象的行为都可以获得预期的结果,那么这个对象就是线程安全的。
保证线程安全就是保证对对象访问的原子性、有序性和可见性:
- 原子性:一段程序只能由一个线程完整的执行完成,而不能存在多个线程版本的干扰。CPU的上下文切换,是导致出现多线程原子性问题的核心原因,JDK提供了synchronized关键字来解决原子性问题。
- 有序性:指的是程序编写的指令顺序和最终CPU运行的指令顺序可能出现不一致的现象,即指令重排。可以通过JDK提供的volatile关键字来解决。
- 可见性:指在多线程环境下,读和写可能发生在不同的线程里,有可能出现莫个线程对共享变量的修改,对其他线程不是实时可见的。
Thread
和Runnable
基本关系
Runnable
接口:Runnnable
是一个函数式接口,只包含一个抽象方法run()
它定义了线程执行的任务,但不包含线程管理功能
Thread
类Thread
是一个类,用于管理和创建线程它实现了
Runnnable
接口,因此它也有run()
方法
多线程
线程调度
线程和任务的关系:
脱离了任务的线程是没有任何意义的
线程对象是通过Thread
类来创建的
任务是通过Runnable
接口来定义的
实现线程:
- 继承
Thread
类 - 实现
Runnable
接口 - 实现
Callable
接口
Thread 线程执行的流程
Thread构造器:无参构造就是不需要指定任务,有参构造可以直接指定线程的任务
1 | public Thread(Runnable target) |
创建线程对象的时候直接传入它的任务
流程:
- 创建线程对象,同时指定任务
- 启动线程,start,就绪状态,等待获取CPU资源
- 一旦拿到CPU资源,开始执行它的任务,调用Thread的run()方法
1 | //使用举例 |
1 | //源码 |
Thread 类中定义了一个Runnable target成员变量
用来接收创建线程对象,传入的任务
- 创建线程对象,传入任务
- 将任务赋值给target成员变量
- 线程执行的时候,操作target成员变量
实现多线程的两种形式对比:
- 继承
线程对象一旦争夺到CPU资源发,就会调用自己的run 方法
继承的缺点在于直接将任务的实现写到线程类当中,耦合度太高
1 | //使用实例, |
- 实现接口
实现接口可以解决上述问题,解耦合
1 | //任务1实现 |
1 | //任务2实现 |
1 | //执行线程 |
线程休眠
sleep方法让谁休眠,不在于谁调用sleep,而在于写在哪个线程中
sleep是Thread类中的抽象方法
线程合并
将指定的线程加入到当前线程中,join
甲乙两个线程,某个时间点在甲线程中调用了乙线程的 join 方法,表示从当前时刻起,线程乙合并到了线程甲中,线程乙会独占 CPU 资源,线程甲进入阻塞状态,当线程乙全部执行完毕之后,线程甲继续执行
调的谁的join()方法,谁先执行
1 | public class JoinRunnable umplements Runnable{ |
1 | public class Test{ |
join() 无参数
join(long millis) 传毫秒参数
区别:合并进来的线程任务执行情况,join() 合并进来的线程会一直占用 CPU 资源,直到任务执行完毕,join(long millis) 合并进来的线程不会一直占用 CPU 资源,而是根据时间参数占用
线程同步
Java 中允许多线程并行访问,同一个时间段内多个线程同时完成各自操作
当多个线程同时操作一个共享数据时,可能会导致数据不准确的情况出现
线程同步就是给线程上锁
同步和异步
同步:两个线程按顺序执行
异步:两个线程同时执行
上锁是实现同步的一种方式
同步是最终的结果,上锁是实现该结果的具体方式
上锁,给要上锁的方法添加 synchronized
关键字
1 | public class Account implements Runnable { |
1 | public class Test { |
synchronized 在进行锁定的时候,还需要判断被锁定的资源有几个,如果被锁定的资源只有一个,则可以实现同步,如果被锁定的资源不止一个,则仍然不会实现同步
- 修饰实例方法:锁定当前对象实例(
this
)。 - 修饰静态方法:锁定当前类的
Class
对象。 - 修饰代码块:可以指定锁定的对象(任意对象)。
synchronized 可以修饰实例方法,也可以修饰静态方法,但是两者在使用上有区别
判断线程是否会同步,只需要判断被锁定的元素(方法,对象)内存中有几份,如果只有一份,则多线程会实现同步(排队执行),如果是多份,则多线程不会实现同步(同时执行)。
java中保证线程安全的方式有哪些
针对原子性:
1)JDK提供了非常多的Atomic类
2)Java提供了各种锁的机制,来保证锁内的代码块在同一时刻只能被一个线程执行,比如synchronized关键字加锁
什么是java中的线程同步
线程同步是指在多线程环境下,为了避免多个线程对共享资源进行同时访问,从而引发数据不一致或其他问题的一种机制。它通过对关键代码加锁,使得同一时刻只有同一个线程能够访问共享资源。
拓展:
Java中常见的同步方式:
synchronized
Java提供的加锁关键字,用于在方法或代码块上加锁,以确保同一时刻只有一个线程能够执行被同步的方法或者代码块
在synchronized可以使用wait()、notify()和notifyAll()实现条件等待通知
- wait():当前线程进入等待状态,直到被其他线程唤醒。必须在同步块或者被同步的方法中使用。
- notify():唤醒一个等待的线程。如果有多个线程在等待,同一时刻只能唤醒一个。
- notifyAll():唤醒所有等待的线程
ReentrantLock
是JUC(java.util.concurrent)提供的可重入锁,相比synchronized它更加灵活。
ReentratLOck使用Condition 对象来提供更灵活的等待/通知机制。每个ReentrantLock可以创建一个或多个Condition对象,通过newCondition()方法创建。
- await():使当前线程等待,直到收到信号或被中断。
- sign():唤醒一个等待线程
- signAll():唤醒所有等待线程
什么是协程?java支持协程吗?
协程是一种轻量级的线程,它允许在执行过程中暂停并在之后恢复执行,而无需阻塞线程。与线程相比,协程是用户调度,效率更高,因为它不涉及操作系统内核。
Java一开始没有原生支持协程,但在Java19中通过Project Loom
引入了虚拟线程,最终在Java21中确认。它提供了类似协程的功能。
协程特点:
- 轻量级:与传统线程不同,协程在用户态切换,不依赖内核态的上下文切换,避免了线程创建、销毁和切换的高昂成本。
- 非抢占式调度:协程的切换又程序员控制,可以通过显示的
yield
和await
来暂停和恢复执行,避免了线程中断问题。 - 异步化编程:协程可以让异步代码像同步代码一样,使代码结构更加简洁清晰。
线程的生命周期在Java中是如何定义的
在Java中,线程的生命周期可以细化为以下几个状态:
- New初始状态):线程对象创建后但未使用start()方法。
- Runnable(可运行状态):调用start()方法后,线程进入就绪状态,等待CPU调度。
- Blocked(阻塞状态):线程试图获取一个对象而被阻塞。
- Waiting(等待状态):线程需要被显示唤醒才能继续执行。
- Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了等待时间,超时后会报警。
- Terminated(终止状态):线程执行完成或因异常退出。
Java中线程如何通信
在Java中,线程之间的通信是指多个线程协同工作,主要实现方式包括:
1)共享变量:
- 线程可以通过访问共享内存变量来交换信息(需要注意同步问题)
- 共享的也可以是文件,比如写入同一个文件来通信
2)同步机制:
- synchronized:Java中的同步关键字,用于确保同一时刻只有一个线程可以访问共享资源,利用Object类提供的
wait()
、notify()
和notifyAll()
实现线程之间的等待/通知机制。 - ReentrantLock:配合Condition提供了
await()
、sign()
和signAll()
的等待/通知机制
Lambda表达式
概念
- 基本语法:
1 | (parameters) -> expression 或 (parameters) ->{ statements; } |
Lambda表达式由三部分组成:
- paramaters:类似方法中的形参列表,这里的参数是函数式接口里的参数。这里的参数类型可以明确的声明也可不声明而由JVM隐含的推断。另外当只有一个推断类型时可以省略掉圆括号。
- ->:可理解为“被用于”的意思
- 方法体:可以是表达式也可以代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不反回,这里的代码块块等同于方法的方法体。如果是表达式,也可以返回一个值或者什么都不反回。
- 函数式接口
定义:一个接口有且只有方法
- 如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口
- 如果我们在某个接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口,这样如果有两个抽象方法,程序编译就会报错的。所以,从某种意义上来说,只要你保证你的接口中只有一个抽象方法,你可以不加这个注解。加上就会自动进行检测的。
在集合中的使用
Collection接口
forEach()方法
1
2List<String> fruits = Array.asList("Apple","Banana","Orange");
fruits.forEach(fruit -> System.out.pringln("I like " + fruit));removeIf()方法
使用Lanbda表达式来移除集合中满足特定条件的元素
1
2List<INteger> numbers = new ArrayLIst<>(Array.asList(1,2,3,4,5));
numbers.removeIf(n->n%2==0);spliterator()方法
返回一个可用于并行迭代集合的Spliterator对象
1
2
3
4List<String> fruits = Arrays.asList("Apple","Banana","Orange");
Spliterator<String> spliterator = fruits.spliterator();
spliterator.forEachRemaining(fruit -> System.out.println(fruit));stream()方法
返回一个顺序流,用于对集合中的元素进行顺序操作。可以与forEach()方法结合使用,对集合中的每个元素执行特定操作。
1
2List<String> names = Arrays.asList("Alice","Bob","Charlie");
names.stream().forEach(name -> System.out.pringln("Hello,"+name));
List接口
replaceAll()方法
使用Lambda表达式替换列表中的所有元素
1
2List<Integer> numbers = new ArrayLIst<>(Arrays.asList(1,2,3,4,5));
numbers.replaceAll(n->n*2);sort()方法
使用Lambda表达式对列表进行排序
1
2
3List<String> names = new ArrayList<>(Arrays.asLIst("Alice","Bob","Charlie"));
names.sort((name1,name2) -> name1.compareToIgnoreCase(name2));//根据名称的字母顺序排序
Map接口
forEach()方法
使用Lambda表达式对Map中的每个键值对执行特定的操作。
1
2
3
4
5
6Map<String,Integer> scores = new HashMap<>();
scores.put("Alice",90);
scores.put("Bob",80);
scores.put("Charlie",95);
scores.forEach((name,score) -> System.out.println(name+":"+score));replaceAll()方法
使用Lambda表达式替换Map中的所有值
1
2
3
4
5
6Map(String,INteger) scores = new HashMap<>();
scores.put("Alice",90);
scores.put("Bob",80);
scores.put("Charlie",95);
scores.replaceAll((name,score) -> score + 5);putIfAbsent()方法
使用LAmbda表达式在Map中插入键值对,仅当键不存在时才插入。Lambda表达式用于定义要插入的值,接受键作为参数。
1
2
3
4
5Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 80);
scores.putIfAbsent("Charlie", 95); // 插入键值对"Charlie=95"