Java总结(更新中)
Java基本类型
强类型语言
Type | byte | short | int | long | double | float |
---|---|---|---|---|---|---|
Size(Byte) | 1 | 2 | 4 | 8 | 8 | 4 |
int hex = 0xCAFE;
int oct = 0777;
long l = 40000L;
如果 short byte 需要使用强制类型转换才能保持原类型
整数/0 Exception
非零浮点数/0 无穷大 Infinity
0.0/0 NaN (Not a number) 例如还有sqrt(-1)
使用BigDecimal计算没有误差
char类下是使用Unicode编码
boolean与int不能进行相互转换
浮点数计算误差问题
浮点运算存在误差,如2.0-1.1=8.89....
原因在于二进制小数无法精确的表达10进制小数,小数表示分为尾数,阶码
Java相关概念
Java applet 在网页上运行的Java程序 (被JavaScript,flash取代)
sdk jdk的早期版本,软件开发工具
JAR java Archive File java档案文件 一种兼容zip的压缩文件
jar cf test.jar test 创建jar包
不同版本的区别在于JavaAPI库内容
- Java SE java标准版
- Java EE java企业版 (相当于是API的超集)
- Java ME java微型版
值传递和引用传递
值传递是对基本变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。
引用传递是对对象型变量而言的,传递的是对象地址的一个副本。
一般认为Java内都是值传递,因为java传入的非基本类型值是引用。
深拷贝和浅拷贝
浅拷贝与深拷贝的区别
浅拷贝与深拷贝的区别在于前者复制不了引用类型的域
数组拷贝(深拷贝)
Array.copyOf(arr,length)
将arr的值拷贝一份出来,arrLength新数组长度
Object.clone 浅拷贝
关于克隆clone
自定义类实现克隆的方法:
实现标记接口Cloneable
实现Object clone()方法,通过(XX)super.clone()返回克隆对象
Object类提供的Clone机制只是简单的复制,存储的对象的域对象还是指向同一个地址。
(浅拷贝)只是克隆对象的成员变量值,不会对引用的对象进行克隆。
clone机制高效,比静态的copy方法快两倍。
动态绑定和静态绑定
静态绑定
程序运行前就知道方法所属了。
动态绑定
程序运行过程中,将函数调用和函数定义(绑定)对应起来。也可以说是找到方法是属于哪个类的。
在静态绑定中,绑定可以在运行时或编译时解析。
所有static,final和private方法的绑定过程都在编译时完成。
类型比较
Object的hashcode方法是本地方法,通过c或c++实现的,返回对象的内存地址。
对于HashMap中的key的比较,需要重写hashCode和equals两个方法。
在满足了hashcode值相等的前提下equals返回为true时,key才算相同。
Comparable接口和Comparator接口
comparable
接口包含一个返回值为int的compareTo(T t)
方法,常常表示这个类是可比较的,如包装类,枚举类。Arrays.binarySerach()
就是需要一个实现Comparable接口的List,不然不能进行二分查询。
comparator
接口包含两个抽象方法,分别是返回值为int的compara(T o1,T o2)
方法和返回值为boolean的equals(Object obj)
方法。因为每个类默认实现Object,所以equals可以选择性覆盖。这个接口称为比较器。描述的是比较策略,与对象(类)无关。但是本人想不通这个接口里设立这个equals方法的意义是什么。Collections.sort()
这个容器排序方法就可以传入一个比较策略(策略设计模式)来比较集合元素。同理Collections.binarySearch
方法也是。
继承和多态
Father one = new Son();
此处引用one是用其子类构造,但是被向上转型成父类的类型了
向下变型(父类转子类)类型转换会有个类型转换异常,因而常常使用(one instanceof Father)
判断
多态
overload和override是Java多态性的不同表现,前者称为重载后者称为重写,覆盖。
overload可以让改变返回值的类型——这称为可协变的返回类型
多态可以是使用一个父类型(接口)可以初始化不同行为(不同派生类)的对象
多态可以是同一个方法拥有不同的类型
可协变的返回类型
子类方法覆盖父类方法,子类方法返回类型可以和父类方法不一致
但是返回类型限定为父类方法返回类型的子类型 参考<? extends Type>
因而这个限于引用类型,如果返回值是基本类型的话还是得保持一致的
内联
早期java中,如果一个方法没有被覆盖且很短,编译器会对其优化处理,即内联
- final类不能被覆盖,final方法不能覆盖
- 接口中的default方法不需要被实现,相当于普通父类的普通方法。
如果一个类继承了一个类并实现了一个接口,而其父类中有个和接口中默认方法同名(当然也同参)的普通方法。
那么编译器要求实现该方法。当然两个接口也适用。 - 父类throws的异常是子类throws异常的超集,子类方法的访问权限不能少于父类
- 静态内部类可以去继承别的静态内部类,但是其父类的静态方法不能被重载。
- 因为方法覆盖是基于运行时动态绑定的
接口和抽象类
声明方法而不去实现它的类被称为抽象类。
接口所有普通方法都不能自己实现,可以有default,statc方法(Java8)这些被实现的方法,
抽象类没有default关键字,有staic方法。
接口的域自动设为静态常量。
接口默认是default,一般设为public
接口和抽象类的区别
首先在意义上,接口是一种规范,定义了类的标准,而抽象类是类的高度抽象化。
用法上,接口的一般方法(defalut,static)都不能自己实现。抽象类可以有可实现的方法。
抽象类可以implements接口,继续抽象。抽象类的中的main入口可以被调用。
标记接口 没有方法,唯一目的是可以用instanceof进行类型检查。如xx instanceof Cloneable
abstract不能与final同时修饰一个类。
枚举类
- 一个java源文件只能定义一个public访问权限的枚举类
- 默认继承了 java.lang.Enum 因此不能显示继承其他父类
- Enum实现了Serializable Comparable接口
- enum默认的定义的非抽象类默认是final修饰,因此不能派生出子类
- 构造器只能是private
- 枚举类实例必须在第一行声明列出
如
enum XXEnum{ A,B,C,D;}
- 使用某个实例 XXEnum.A
对于switch而言,若switch的控制表达式使用的枚举类型时,
case表达式中的值直接使用枚举值的名称,无需枚举类限定。如直接输入A - Eunm的常用方法
int compareTo(E o)和另一个枚举类实例比较
int ordinal() 返回枚举类实例的索引值
valueOf(enumType,Stirng) 返回指定名称的枚举值
- 枚举类实现接口时,给所有声明的实例通过匿名内部类去实现接口定义的方法。这样可以让不同的枚举实例实现不同的行为。
同样,可以给枚举类安排一个抽象方法,让枚举实例通过匿名内部类分别实现。
内部类
内部类可以访问所在外部类的数据(这个概念叫闭包),包括私有。但对同一个包下其他类隐藏。
内部类有个隐式引用,指向创建它的外部类对象,一般是Outer.this.xx引用外部类的域
外部类访问内部类 this.new Inner();
普通类只有public和默认不修饰,内部类可以是static,protect,private
内部类拥有独立的名称空间,当外部类被其他类继承的时候,内部类没有被继承。
局部内部类
在局部作用域内的内部类,如方法体内。
不能被public、private,只能在局部内访问。
局部内部类可以访问局部变量,但是该局部变量必须是final的。可以不声明,但是这个局部变量的值一定不能改变。
匿名内部类
不命名直接创建类对象
常用定义实现某接口的类,如Comparator接口
静态内部类
为了单纯地隐藏该类到另一个类内部,并不需要内部类引用外部类。当然,外部类的静态域还是正常访问的。
static修饰类的话,只能是静态内部类。
异常类
Throwable(基类)
Error 运行时系统内部错误
Exception
RuntimeException 程序错误导致的异常
错误的类型转换、数组访问越界、访问空指针
其他异常 程序本身没问题
如I/O错误这些 文件结尾读取数据、打开错误格式的URL、用不存在的class查找Class错误
Error和RuntimeException称为为未检查异常。
其他为已检查异常,拥有异常处理器。
未检查异常要么不可控制(Error),要么应该避免发生(RuntimeException)。
堆栈跟踪
stack trace 一个方法调用的列表, 包含程序执行过程中方法调用的特定位置。
断言
声称(断言)某个东西是某东西(符合某个要求),若不是则抛出异常。
-
父类throws的异常是子类throws异常的超集
-
一般捕获那些知道如何处理的异常,抛出那些不知道怎么处理的异常。
-
在catch中抛出异常(抛出异常链)
如 catch(XXException e){throw new BBExceptiom..} -
finally 即使return也得先执行finally
throws 提前声明了可能会发生什么异常,那么运行时,将不会正常执行,构造器将会抛出异常对象,runtime就会开始搜索异常处理器。
反射:获取Runtime类型信息的途径
常用功能:
- 能够分析类能力的程序
- 在运行中分析类的能力
- 在运行中查看对象
- 实现数组的操作代码
程序运行期间,Java Runtime系统为所有对象维护一个runtime,保存着对象所属的类足迹。
虚拟机利用runtime信息选择相应的方法执行。
Class类用于访问这些信息
反射机制的内容
- 检查类的结构
Field、Method、Constructor
用以获取类的域,方法,构造器的相关信息。
在java.lang.Class中,
分别对应着getFields() getDeclaredFields getMethods()
- 查看编译时还不清楚的对象作用域
反射机制的默认行为受限于java的访问控制权限。
即private等静态域,通过getField.getName之类的访问会产生异常 - 使用反射编写泛型数组
如,数组扩展,扩展的新数组需要确定类型。通过Array.newInstance
method.invoke(object this,Object...args)
第一个参数是对应对象句柄,静态方法可省略,可设置为null,第二个方法是对应方法参数,返回值是Object
getConstructor()
方法获得构造器对象,配合newInstance()
方法可以创建对象
泛型
泛型程序设计 generic programming
泛型类看作普通类的工厂。
泛型类
public class XX<T>{
public XX(){}
public T getA(){};
public void setA(T xx){};
}
/*普通类中的泛型方法*/
class XX{
public <T> T getA(T[] xx){
return xx[0];
}
}
/*类型变量放在修饰符和返回类型之间
XX.<String>getA();
这里返回一个String类型的值,其实String可以省略,因为编译器可以推断出所调用的方法。
泛型类型变量的限定
假如在一个泛型方法中,泛型变量的类型是限定的。
比如是实现某个接口的类型,这个类型的范围就缩小了,
这时候就需要在方法声明处修改,如:
public static <T extends Comparable> T fun(){..}
此时,这个T表示的是所有实现Comparable接口的类型,
限定多个事,用&隔开。如T extends Comparable & Serializable>
擦除:删除类型参数后的泛型类型名。
如XX<T>的原始类型是XX,类定义其中的T用Object替换
因为T是一个无限定的变量,所以直接用Object替换。
如果是<T extends Comparable & Serialiizable>, 则用Comparable替换
- 当程序调用泛型方法的时候,如果擦除返回类型,编译器将插入强制类型转换符。
Pair<XX> xxs = ...;
XX xx = xx.getFirst();
这里getFirst()返回的XX被擦除成Object,编译器自动插入强制转换成XX.
-
虚拟机里没有泛型,只有普通的方法和类;
所有类型参数都用他们的限定类型替换;
为保持类型安全性,必要时插入强制类型转换。 -
泛型仅仅是java的一颗语法糖,它不会影响Java虚拟机生成的汇编代码。
在编译阶段,虚拟机就会把泛型的类型擦除,还原成没有泛型的代码,
顶多编译速度稍微慢一些,执行速度是完全没有什么区别的。
类型变量的限定
/*此处泛型限定为实现所有Comparable接口的类型*/
public static <T extends Comparable> Pair<T> minMax(T[] a){
if(a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for(int i = 0; i < a.length; i++){
if(min.compareTo(a[i]) > 0) min = a[i];
if(max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<T>(min,max);
}
泛型类型的继承规则和通配符类型
<? extends Employee> 可能是Employee也可能是其派生类
<XX<? super Employee> 可能是Employee也可能是其超类
- Manager是Employee的子类
那么XX<Manager>
和XX<Employee>
可以看做是XX<? extends Employee>
的子类型,
XX<? extends Employee>
看做是XX<raw>
的子类型
使用:XX<? extends Employee> xx = new XX<Manager>();
注意:拿List作例子,对于List<? extends Manager>
,不能使用add(T t)这样的方法,但get可以。
可以这么理解:这个容器存放的是所有实现Manager的类(包括自己)。我们不知道add的是什么类型,但是get方法却可以,因为我传出去的对象无论如何都能被Manager接收。可以add(null)
- 同理,对于
XX<? super Manager>
,XX<Employee>
和XX<Object>
是XX<? super Manager>
的子类
是XX<?>
的子类型,这里XX<?>是XX<raw>
的子类型。
使用:<XX<? super Employee> xx = new XX<Object>();
注意:拿List作例子,对于List<? super Manager>
类型不能使用get()这样的方法,add可以。
可以这么理解:这个通配符类型代表的是所有Manager的超类(包括自己)。要是传入一个Manager的派生类(Manager的超类不行),肯定都是能被Manager或者Manager的超类接收的,但是get方法却不可以,除了Object,其他都接收不了。
-
总言之,带有超类型限定的通配符可以向泛型对象写入,
带有子类型限定的通配符可以从泛型对象读取。 -
无限通配符 XX<?>
其他
- 泛型不能用基本类型的原因是擦除之后Object不能存储基本类型。
- 运行时类型查询只适用于原始类型,泛型当做原始类型处理。
- 泛型类拓展Throwable不合法,但throws异常可以使用类型变量,例如:
public static <T extends Throwable> void doWork(T t) throws T{
try{
do work
}catch (Throwable realCause){
t.initCause(realCause);
throw t;
}
}
- 禁止使用参数化类型的数组,不合法。
因为数组会记住元素的存储类型,如果可行,擦除之后数组只能记住擦除之后类型,强转不方便。 - 不能再静态域和静态方法中使用泛型变量
- 泛型类不支持内部类型的
- Class类是泛型的,
String.class
实际上是Class<String>
类的对象
集合框架
基本接口 Colllection,Map
迭代器
- iterator.remove() 删除上次调用next返回的元素,在之前没用next的话就不合法,就会抛出异常
- ListIterator
add(E)
previous() 对应next()方法
hasPrivious()
Set
- 对于TreeSet的使用
通过实现Comparable<T>接口的compareTo方法来比较类的先后
如果要实现不同set实例不同的比较策略:
实现Comparator接口的compare(T a,T b)方法
然后将这个类的对象传给TreeSet的构造器,那么该TreeSet实例的的排序策略就定了
常常是通过匿名内部类实现,对应对象常常被称为函数对象。
- 集合子范围 subrange
List group = staff.subList(10,20) [10,20)
group.clear() 清除子范围
SortedSet<E> subSet(E from, E to)
SortedSet<E> headSet(E to)
SOrtedSet<E> headSet(E from) 返回大于等于from,小于to的所有元素子集
- 相应的,map也有相似的方法
SortedMap<K,V> subMap(K from,K to)
SortedMap<K,V> headMap(K to)
SortedMap<K,V> tailMap(K from) 返回键落在指定范围内的所有元素
- 交集
Set<String> result = new HashSet<String>(a);
result.retainAll(b); //此时result便是ab的交集
视图:可以获得其他实现[集合接口和映射表接口]对象的对象 (可以结合数据库的视图理解)
例如keySet()返回的集合。 它是返回实现Set接口的类对象,这个类的方法对原映射表进行操作。
Array.asList(xx[]) 返回的对象不算ArrayList实例,而是一个视图对象,带有访问底层数组的get和set方法,改变数组大小的方法。
通过视图删除原映射表的内容
比如 view 为 map key的集合子范围,map.keySet().removeAll(view);
Map
Map接口有四个实现类,HashMap,HashTable,LinkedHashMap,TreeMap
- HashMap
允许一条记录的键为null,多条记录的值为null;
需要支持同步时可以用Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。遍历速度更容量有关。 - HashTable 线程安全,继承Dictionary类,不运行键或值为null
- LinkedHashMap 保存了记录的插入顺序,使用iterator遍历时按照插入顺序遍历。也可以在构造是用参数,按照指定规则排序。遍历速度跟实际数据有关。
- TreeMap 实现了SortMap接口,能够将保存的记录根据键排序,默认按键值升序,也可以指定排序比较器
Comparator
。
Collections
Collections里有许多静态方法
Collections.sort(list)
Collections.sort(list,new Comparator);
Collections.sort(list, Collections.reverseOrder(new Comparator)) 逆序排序
Java的排序,基本类型使用快排,引用类型使用归并排序,是一个稳定排序
先将元素转成数组并使用归并排序的变体进行排序,然后再复制回列表。
对于已排序的集合,可用Collections.binarySearch(容器,key/element) 也可以添加一个compartor对象的参数
Collections.min、Collections.max
Collectuons.copy(to,from)
Collections.fill(con,value)
Collections.addAll(con,valuel,value2...)
Collections.replaceAll(con,oldValue,newValue)
其他
-
Hsahtable 小写table 与Vector一样同步
-
Enumeration hasMoreElements nextElement 与迭代器相似
-
Properties 属性映射表 键值对都是字符串 可以保存在文件中,也可以从文件中加载
-
Stack push pop 栈
-
BitSet 位集 存放一个位序列 进行 and or运算
-
ArrayList list = new ArrayList() 默认创建大小为10的数组,一次扩容1.5倍
ArrayList list = new ArrayList(15) 这样的话就直接指定 -
堆是一个可以经过自我调整的二叉树
-
EnumSet 枚举集
EnumMap 键类型为枚举类型 -
NavigatableSet接口
-
Vector使用数组方式存储数据,相比链表插入删除比较慢。线程安全(添加了synchronized),性能比ArrayList差
-
LinkedList 使用双向链表实现存储。比ArrayList更吃内存。
Java线程
创建线程
- 类继承Thread并实现run方法
注意不要实现Thread类或者Runnable对象的run方法。直接调用run方法不会启动新线程
应该使用Thread.start,将会创建一个执行run方法的新线程
- 类实现Runnable的run方法,并将类传给一个Thread对象 Thread(run)
创建来源于两个方法Thread()
Thread(Runable target)
,前者通过继承,后者通过传参
Thread的源码中,只有含有Runnable类型的域,run方法才能执行,
- 启动线程的实质是JVM运行线程,然后通过线程调度器调度
- run方法是JVM直接调用的,因此在代码中使用线程的run方法,是运行在当前线程的,这违背了“多线程”的初衷
- 父线程是守护线程,那么子线程也是;父子线程优先级默认保持一致,生命周期无关系
- 一些方法
void join() 若线程A调用线程B的join方法,那么线程的A的运行将会被暂停,直到B线程执行结束
static void yield() 尝试让当前线程暂停(让调度器重新调度)
中断线程
正常情况下,run方法执行到最后一条语句,线程将中止。
interrupt中断线程。 调用时线程的中断状态将被置位。
thread.isInterrupted()查看线程中断状态
Thread.interrrupted清除终端状态
若线程被阻塞则无法检测。检测时产生InterruptedException
线程状态
Thread.State getState()
返回线程的状态
- Thread.Status 一个枚举类,枚举了线程的状态信息
- new 线程刚刚创建时的状态
- Runable 调用了start方法,变为可运行状态,可能在运行也可能没用运行。
- Blocked 被阻塞 不运行任何代码消耗最少的资源 直到线程调度器重新激活它
想要获得锁,而锁被其他线程持有,那么该线程进入阻塞状态。 - waiting 等待另一个线程通知调度器的状态
- Timed waiting 超时之后,计时等待
- Terminated 自然死亡或者意外死亡
线程优先级
setPriority(int)
设置线程优先级,线程优先级是依赖于系统的。
守护线程
thread.setDaemon(true)
将线程设为守护线程,为其他线程提供服务,如计时线程,垃圾回收线程。
如果只有守护线程的话,那么程序也就结束了。
未捕获异常处理器
线程的run方法不能抛出任何被检测的异常,但是如果不检测的话会导致线程死亡。这时候可以:
- 安装一个处理器——一个实现Thread.UncaughtExceptionHandler接口uncaughtException方法的类。
- 用setUncaughtExceptionHandler方法为任何线程安装一个处理器
- 用Thread的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认处理器。
如果不安装默认处理器,默认处理器为空。
如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象。
线程组是一个可以统一管理的线程集合,默认情况下所有线程属于同一个线程组。
ThradGroup实现了Thrad.UncaughtExceptionHandler接口
多线程和同步问题
concurrent 并发
parallel 并行 并发的极致
- 高并发 处于Runnable的线程数量越多,并发程度越高
线程安全
一个类在单线程和多线程的情况下都能正常运行。
Java实现原子操作的两种方式
- Lock接口 保证一个共享变量一个时刻只能被一个线程访问。
- CAS
除了long,double之外的其他类型变量的写操作都是源自操作(JVM实现的)
用volatile修饰后可保证其原子性
锁
用来保护代码片段,使得任何时候只有一个线程执行被保护的代码。
也可以管理视图进入被保护的代码片段的线程
通过条件对象来管理那些已经进入被保护的代码片段
- 锁的排他性 一个锁只能被一个线程持有,称为互斥锁,mutex
按照JVM来分,分为
- 内部锁 又称监视器
- 显式锁
内部锁
使用synchronized修饰方法或者代码块,修饰的方法称为同步方法
用以保证该方法一次只被一个线程执行,而代码块称为同步块
synchronized(锁句柄){
...
}
这里的锁句柄可以是this,此时会锁句柄为锁,对应的句柄为引导的锁
- 同步静态方法相当于当前类对象XX.class为引导的同步块
- 称为内部锁的原因:线程对内部锁的申请和释放由JVM代由实施
显式锁
Lock接口的实例,默认实现类ReentrantLock
常用方法
void lock() 获取锁
void unlock() 释放锁 一般放在finally块里
boolean tryLock() 尝试获得锁
new ReentrantLock(true) 创建公平锁 (默认是非公平锁)公平锁增加了线程的暂停和唤
synchronized
java的每一个对象都有一个内部锁,一个方法用synchronized来声明,那么该对象的锁将保护整个方法。
对于静态同步方法,对应的便是类对象的内部锁。
public synchronized void method(){
wait();
...
notifyAll();
}
客户端锁定: 不推荐使用
synchronized(obj){...}
线程获得obj的锁
线程安全
当多线程访问一个类时,可以不用考虑这些线程在运行时环境下的调度和交替执行,
并且不需要额外的同步及在调用方代码不必作其他的调度,这个类的行为仍然是正确的。
sleep方法和wait方法的联系和区别
- sleep,wait方法都可以通过interrupt方法被打断线程的暂停状态,
- 如果线程正在处于sleep,wait,join等状态,会立刻抛出InterruptedException
- sleep方法没用释放锁,wait方法释放了锁,使得其他线程可以使用同步控制块
- sleep可以在任何地方使用,需要捕获异常
await 释放锁并进入等待阻塞状态
signalAll 通知等待的线程,激活他们
若是一个线程进入await,而又没有其他等待的线程激活它,那么就进入了死锁
notify() 唤醒在此对象监视器上等待的单个线程。
notifyAll() 唤醒在此对象监视器上等待的所有线程。
wait() 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
wait,notifyAll,notify都是final方法,来自Object
wait 方法使当前线程进入等待状态并释放锁
线程之间如何通信:通过notify和wait
什么要在同步块内呢:因为不同线程之间会随机竞争资源,我们要对共享资源的操作定序
垃圾回收
强制开始垃圾回收
System.gc()
Runtime.getRuntime().gc()
垃圾回收器调用finalize()
finalize()方法返回后,对象消失,垃圾回收机制开始执行。
可以重写finlize方法实现复活该被清理的对象。
强引用:一个对象赋给一个引用就是强引用,比如new一个对象,一个对象被赋值一个对象。
软引用:用SoftReference类实现,一般不会轻易回收,只有内存不够才会回收。
弱引用:用WeekReference类实现,一旦垃圾回收已启动,就会回收。
虚引用:不能单独存在,必须和引用队列联合使用。主要作用是跟踪对象被回收的状态。
String
不可变对象是指一个对象的状态在对象被创建之后就不再变化。
String是一个final类,String底层是char[] 实现的,实现时char[]是final的
不可变的好处:
节省堆空间。
不可变可保证安全性,比如数据库账户密码等,没有办法在不修改地址的情况下修改其值。
线程安全。因为不可变,不可写,读一致。
不可变保证了HashCode码的唯一性,不需要重新计算,适合作为字典的key