21、3*0.1==0.3返回值是什么
false,因为有些浮点数不能完全精确的表示出来。
System.out.println(3*0.1);//0.30000000000000004
System.out.println(0.3);//0.3
System.out.println(3*0.1==0.3);//false
22、a=a+b与a+=b有什么区别吗?
- +=操作符会进行隐式自动类型转换。此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类
型。而a=a+b则不会自动进行类型转换.如:
byte a = 127;
byte b = 127;
b = a + b; // 报编译错误:cannot convert from int to byte
b += a;
以下代码是否有错,有的话怎么改?
short s1= 1;
s1 = s1 + 1;
有错误.short类型在进行运算时会自动提升为int类型,也就是说s1+1的运算结果是int类型,而s1是
short类型,此时编译器会报错.
正确写法:
short s1= 1;
s1 += 1;
+=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错.
23、try catch finally,try里有return,finally还执行么?
执行,并且finally的执行早于try里面的return.
- 结论:
- 1、不管有没有出现异常,finally块中代码都会执行;
- 2、当try和catch中有return时,finally仍然会执行;
- 3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的
值保存起来,不管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数
返回值是在finally执行前确定的; - 4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
24、 Excption与Error包结构
- Java可抛出(Throwable)的结构分为三种类型:
- 运行时异常(RuntimeException);
- 被检查的异常(CheckedException);(也可以说是编译时异常)
- 错误(Error);
24.1、运行时异常
- 定义:RuntimeException及其子类都被称为运行时异常。
- 特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既没有通过throws
声明抛出它,也没有用try-catch语句捕获它,还是会编译通过。- 例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fast机制产生的ConcurrentModificationException异常(java.util包下面的所有的集合类都是快速失败的,“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。
- 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制,这个错叫并发修改异常。
Fail-safe,java.util.concurrent包下面的所有的类都是安全失败的,在遍历过程中,如果已经遍历的数
组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组
上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致性的
表现。ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。
要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样
了。)等,都属于运行时异常。
- 常见的五种运行时异常:
- ClassCastException(类转换异常)
- IndexOutOfBoundsException(数组越界)
- NullPointerException(空指针异常)
- ArrayStoreException(数据存储异常,操作数组是类型不一致)
- BufferOverflowException
24.2、被检查异常
- 定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异
常。 - 特点 : Java编译器会检查它。 此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。被检查异常通常都是可以恢复的。 如:
- IOException
- FileNotFoundException
- SQLException
- 被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的
FileNotFoundException 。
然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的NullPointerException 。
24.3、错误
- 定义 : Error类及其子类。
- 特点 : 和运行时异常一样,编译器也不会对错误进行检查。
- 当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修
复这些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。
OutOfMemoryError、ThreadDeath等等也是一样。 - Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等
- 当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修
25、OOM你遇到过哪些情况,SOF你遇到过哪些情况
25.1、OOM
-
OutOfMemoryError异常除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
-
Java Heap 溢出:
- 一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。- 出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。
- 如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。
- 出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
- 一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。
-
虚拟机栈和本地方法栈溢出
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里需要注意当栈的大小越大可分配的线程数就越少。
- 运行时常量池溢出
- 异常信息:java.lang.OutOfMemoryError:PermGenspace
- 如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。
- 该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
- 方法区溢出
- 方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。
- 异常信息:java.lang.OutOfMemoryError:PermGenspace
- 方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。
25.2、SOF(堆栈溢出StackOverflow)
- StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
- 因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
- 栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。
26、 简述线程、程序、进程的基本概念。以及它们之间关系是什么?
-
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是,同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
-
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
-
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序就是一个进程从创建,运行到消亡的过程。
- 简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。
- 换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
27、Java 序列化中如果有些字段不想进行序列化,怎么办?
- 对于不想进行序列化的变量,使用 transient 关键字修饰。
- transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
27.1、知识补充:序列化与反序列化的理解
- 在Java中,序列化是指将对象转换为字节流的过程,而反序列化则是将字节流转换回对象的过程。这种机制允许我们在网络上传输对象或将对象保存到文件中。
- Java中的序列化与反序列化主要通过实现Serializable接口来实现。当一个类实现了Serializable接口后,它的对象就可以被序列化和反序列化。
- 序列化的过程是将对象的状态信息转换为字节流的过程。通过将对象写入输出流,可以将其保存到文件中或通过网络传输。在序列化过程中,对象的所有成员变量都会被保存,并且可以包含对其他对象的引用。
- 反序列化的过程是将字节流转换回对象的过程。通过从输入流中读取字节流,可以重新创建对象,并恢复其状态信息。在反序列化过程中,对象的所有成员变量都会被恢复,并且可以重新建立对其他对象的引用。
- 序列化和反序列化在Java中有很多应用场景,例如:
- 对象的持久化:将对象保存到文件或数据库中,以便后续读取和使用。
- 远程方法调用(RPC):在分布式系统中,可以通过序列化和反序列化来传输对象参数和返回值。
- 缓存:将对象序列化后存储在缓存中,以提高系统性能。
需要注意的是,不是所有的对象都可以被序列化。一些特殊的对象,如线程、文件流等,是不能被序列化的。
28、说说Java 中 IO 流
-
Java 中 IO 流分为几种?
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流;
- 按照流的角色划分为节点流和处理流。
-
Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
-
按操作方式分类结构图:
-
按操作对象分类结构图:
29、 Java IO与 NIO的区别(补充)
NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,
NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标
准输入输出NIO,另一套就是网络编程NIO。
30、java反射的作用于原理
-
定义:反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。
- 这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
-
哪里会用到反射机制?
jdbc就是典型的反射
Class.forName('com.mysql.jdbc.Driver.class');//加载MySQL的驱动类
这就是反射。如hibernate,struts等框架使用反射实现的
- 反射的实现方式:
- 第一步:获取Class对象,有4中方法:
- 1)Class.forName(“类的路径”);
- 2)类名.class
- 3)对象名.getClass()
- 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
- 第一步:获取Class对象,有4中方法:
- 实现Java反射的类:
- 1)Class:表示正在运行的Java应用程序中的类和接口 注意: 所有获取对象的信息都需要Class类来实现。
- 2)Field:提供有关类和接口的属性信息,以及对它的动态访问权限。
- 3)Constructor:提供关于类的单个构造方法的信息以及它的访问权限
- 4)Method:提供类或接口中某个方法的信息
- 反射机制的优缺点:
- 优点:
- 1)能够运行时动态获取类的实例,提高灵活性;
- 2)与动态编译结合
- 缺点:
- 1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
- 解决方案:
- 1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度;
- 2、多次创建一个类的实例时,有缓存会快很多
- 3、ReflectASM工具类,通过字节码生成的方式加快反射速度
- 解决方案:
- 2)相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
- 1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
- 优点:
31、说说List,Set,Map三者的区别?
- List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象)、有序
的对象。(List(列表)是有序的集合,可以包含重复的元素。它允许通过索引访问元素,并且可以根据需要动态调整大小。常见的List实现类有ArrayList和LinkedList。) - Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。(Set(集合)是无序的集合,不允许包含重复的元素。它提供了高效的查找和插入操作,但不支持按索引访问元素。常见的Set实现类有HashSet和TreeSet。)
- Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引
用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。(Map(映射)是一种键值对的集合,每个键都是唯一的。它提供了根据键快速查找值的功能。常见的Map实现类有HashMap和TreeMap。)
32.、Object 有哪些常用方法?大致说一下每个方法的含义
- 下面是对应方法的含义:
-
clone 方法:保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出CloneNotSupportedException 异常,深拷贝也需要实现 Cloneable,同时其成员变量为引用类型的也需要实现 Cloneable,然后重写 clone 方法。
-
finalize 方法:该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方法。
-
equals 方法:该方法使用频率非常高。一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。
-
hashCode 方法:该方法用于哈希查找,重写了 equals 方法一般都要重写 hashCode 方法,这个方法在一些具有哈希功能的 Collection 中用到。一般必须满足
obj1.equals(obj2)==true
。可以推出obj1.hashCode()==obj2.hashCode()
,但是hashCode 相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。- JDK 1.6、1.7 默认是返回随机数;
- JDK 1.8 默认是通过和当前线程有关的一个随机数 + 三个确定值,运用 Marsaglia’s xorshift
scheme 随机数算法得到的一个随机数。
-
wait 方法:配合synchronized 使用,wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait() 方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。-
- 其他线程调用了该对象的 notify 方法;
-
- 其他线程调用了该对象的 notifyAll 方法;
-
- 其他线程调用了 interrupt 中断该线程;
-
- 时间间隔到了。
- 此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。
-
-
notify 方法:配合synchronized 使用,该方法唤醒在该对象上等待队列中的某个线程(同步队列中的线程是给抢占 CPU 的线程,等待队列中的线程指的是等待唤醒的线程)。
-
notifyAll 方法:配合 synchronized 使用,该方法唤醒在该对象上等待队列中的所有线程。
-
- 总结
- 只要把上面几个方法熟悉就可以了,toString 和 getClass 方法可以不用去讨论它们。该题目考察的是对 Object 的熟悉程度,平时用的很多方法并没看其定义但是也在用,比如说:wait() 方法,equals() 方法等。
Class Object is the root of the class hierarchy.Every class has Object as a superclass.
All objects, including arrays, implement the methods of this class.
- 大致意思:Object 是所有类的根,是所有类的父类,所有对象包括数组都实现了 Object 的方法。
33、在 Java 中,为什么不允许从静态方法中访问非静态变量?
- 静态变量属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问;
- 非静态变量属于类的对象,只有在类的对象产生时,才会分配内存,通过类的实例去访问;
- 静态方法也属于类本身,但是此时没有类的实例,内存中没有非静态变量,所以无法调用。
34、获取一个类Class对象的方式有哪些?
- 搞清楚类对象和实例对象,但都是对象。
- 第一种:通过类对象的 getClass() 方法获取,细心点的都知道,这个 getClass 是 Object 类里面的方法。
User user=new User();
//clazz就是一个User的类对象
Class<?> clazz=user.getClass();
- 第二种:通过类的静态成员表示,每个类都有隐含的静态成员 class。
//clazz就是一个User的类对象
Class<?> clazz=User.class;
- 第三种:通过 Class 类的静态方法 forName() 方法获取。
Class<?> clazz = Class.forName("com.knife.User");
35、ArrayList 和 LinkedList 的区别有哪些?
-
ArrayList
- 优点:ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
- 缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。
-
LinkedList:
- 优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作,LinkedList 比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。
- 缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低。
-
适用场景分析
- 当需要对数据进行对随机访问的时候,选用 ArrayList。
- 当需要对数据进行多次增加删除修改时,采用 LinkedList。
- 如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用 ArrayList。
- 当然,绝大数业务的场景下,使用 ArrayList 就够了,但需要注意避免 ArrayList 的扩容,以及非顺序的插入。
36、用过 ArrayList 吗?说一下它有什么特点?
- 只要是搞 Java 的肯定都会回答“用过”。所以,回答题目的后半部分——ArrayList 的特点。可以从这几个方面去回答:
-
Java 集合框架中的一种存放相同类型的元素数据,是一种变长的集合类,基于定长数组实现,当加入数据达到一定程度后,会实行自动扩容,即扩大数组大小。
-
底层是使用数组实现,添加元素。
- 如果 add(o),添加到的是数组的尾部,如果要增加的数据量很大,应该使用 ensureCapacity() 方法,该方法的作用是预先设置 ArrayList 的大小,这样可以大大提高初始化速度。
- 如果使用 add(int,o),添加到某个位置,那么可能会挪动大量的数组元素,并且可能会触发扩容机制。
-
高并发的情况下,线程不安全。多个线程同时操作 ArrayList,会引发不可预知的异常或错误。
-
ArrayList 实现了 Cloneable 接口,标识着它可以被复制。注意:ArrayList 里面的 clone() 复制其实是浅复制。
-
37、有数组了为什么还要搞个 ArrayList 呢?
- 通常我们在使用的时候,如果在不明确要插入多少数据的情况下,普通数组就很尴尬了,因为你不知道需要初始化数组大小为多少,而 ArrayList 可以使用默认的大小,当元素个数到达一定程度
后,会自动扩容。- 可以这么来理解:我们常说的数组是定死的数组,ArrayList 却是动态数组。
38、说说什么是 fail-fast?
- fail-fast 机制是 Java 集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。
- 例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。这里的操作主要是指 add、remove 和 clear,对集合元素个数进行修改。
- 解决办法:建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。
- 可以这么理解:在遍历之前,把 modCount 记下来 expectModCount,后面expectModCount 去和 modCount 进行比较,如果不相等了,证明已并发了,被修改了,于是抛出ConcurrentModificationException 异常。
- 解决办法:建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。
- 例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。这里的操作主要是指 add、remove 和 clear,对集合元素个数进行修改。
39、说一下HashMap的实现原理?
- HashMap是Java中常用的数据结构,它基于哈希表实现。下面是HashMap的实现原理:
-
- 哈希函数:HashMap使用哈希函数将键映射到哈希表的索引位置。哈希函数可以是任意的,但是它应该能够将键均匀地映射到不同的索引位置,以减少冲突。
-
- 数组和链表/红黑树:HashMap内部使用一个数组来存储数据。每个数组元素称为桶(bucket),每个桶可以存储一个或多个键值对。如果多个键值对映射到同一个桶上,它们会以链表或红黑树的形式存储。
-
- 冲突解决:当多个键值对映射到同一个桶上时,就会发生冲突。HashMap使用链表或红黑树来解决冲突。当链表长度超过一定阈值时,链表会转换为红黑树,以提高查找效率。
-
- 扩容:当HashMap中的元素数量超过负载因子(默认为0.75)与当前容量的乘积时,会触发扩容操作。扩容会创建一个新的数组,并重新计算每个键值对在新数组中的位置。
-
- 键的唯一性:HashMap中的键是唯一的,如果插入一个已经存在的键,则会覆盖原有的值。
-
- 迭代顺序:HashMap中的元素没有固定的迭代顺序,它们的顺序可能会随着扩容等操作而改变。
-
40、HashMap 中的 key 我们可以使用任何类作为 key 吗?
- 平时可能大家使用的最多的就是使用 String 作为 HashMap 的 key,但是现在我们想使用某个自定义类作为 HashMap 的 key,那就需要注意以下几点:
- 如果类重写了 equals 方法,它也应该重写 hashCode 方法。
- 类的所有实例需要遵循与 equals 和 hashCode 相关的规则。
- 如果一个类没有使用 equals,你不应该在 hashCode 中使用它。
- 咱们自定义 key 类的最佳实践是使之为不可变的,这样,hashCode 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode 和 equals 在未来不会改变,这样就会解决与可变相关的问题了。