打乱List集合元素顺序
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
Collections.shuffle(list);
classpath
public static void main(String[] args) {
//getResource方法中的参数不以’/'开头时,是从ClassPath根下获取;
URL resource = DispatcherServlet.class.getClassLoader().getResource("");
System.out.println(resource);
}
输出:
file:/D:/my-project-demo/springmvc-ljw/mvc-demo-ljw/target/classes/
Properties
private final String handlerAdapter = "spring.bean.handlerAdapter";
Properties prop = new Properties();
InputStream is = DispatcherServlet.class.getResourceAsStream("/application.properties");
prop.load(is);
//从字节输入流中读取键值对
String className = prop.getProperty(handlerAdapter);
1. Class.getResourceAsStream(String path):
path 不以’/'开头时默认是从此类所在的包下取资源,以’/'开头则是从ClassPath根下
获取。其只是通过path构造一个绝对路径,最终还是由ClassLoader获取资源。
2. Class.getClassLoader.getResourceAsStream(String path):
默认则是从ClassPath根下获取,path不能以’/'开头,最终是由ClassLoader获取资源。
3. ServletContext. getResourceAsStream(String path):
默认从WebAPP根目录下取资源,Tomcat下path是否以’/'开头无所谓,当然这和具体的容器实现有关。
静态代码块
类的Static代码块,也就是静态代码块,只会执行一次,是在类被加载的时候执行。因
为每个类只会被加载一次,所以静态代码块也只会被执行一次。而构造方法每次生成一
个对象的时候都会调用类的构造方法,所以new一次就会调用构造方法一次。静态代码
块的作用也是完成一些初始化工作。首先执行静态代码块,然后执行构造方法。静态代
码块在类被加载时候执行,而构造方法是在生成对象时候执行;要想调用某个类来生成
对象,首先需要将类加载到Java虚拟机上(JVM),然后由JVM加载这个类来生成对象。
如果继承体系中既有构造方法,又有静态代码块,那么首先执行最顶层的类的静态代码
块,一直执行到最底层的类的静态代码块。然后再去执行最顶层的类的构造方法,一直执
行到最底层的类的构造方法。注意:静态代码块只会执行一次。
不能在静态方法中访问非静态成员变量;可以在静态方法中访问静态成员变量。可以在
非静态方法中访问静态的成员变量。不能在静态方法中使用this关键字。
静态变量
静态变量 被赋值后 保存全局, 任何地方都可以被使用.
静态变量 可以 被修改.
静态变量线程不安全.
一种是被static关键字修饰的变量,叫类变量或者静态变量
类的静态变量在内存中只有一个,java虚拟机在加载类的过程中为静态变量分配内存,静
态变量位于方法区,被类的所有实例共享。静态变量可以直接通过类名进行访问,其生命
周期取决于类的生命周期。
而实例变量取决于类的实例。每创建一个实例,java虚拟机就会为实例变量分配一次
内存,实例变量位于堆区中,其生命周期取决于实例的生命周期。
JAVA中初始化的顺序:
加载类;
静态变量初始化;
静态块;【其只能调度静态的,不能调度非静态的】
成员变量;
构造方法;
public class ServerBean {
private String name;
private int port;
public void injectionConf(){
new ServerConf(this.name,this.port);
}
set和get
}
public class ServerConf {
private static ServerConf serverConf;
private String serverName;
private int port;
public ServerConf(String serverName, int port) {
this.serverName = serverName;
this.port = port;
serverConf = this;
}
public static ServerConf getServerConf() {
return serverConf;
}
}
public class ServerInitializer {
private Map<String, Object> rpcServiceMap;
public ServerInitializer(Map<String, Object> rpcServiceMap) {
this.rpcServiceMap = rpcServiceMap;
}
public void init() throws Exception {
ServerConf serverConf = ServerConf.getServerConf();//获取静态变量
System.out.println("-------"+ JSON.toJSONString(serverConf));
}
}
@Test
public void test() throws Exception {
ServerBean serverBean = new ServerBean();
serverBean.setName("uiuiu");
serverBean.setPort(1235);
serverBean.injectionConf();//给静态变量赋值
new ServerInitializer(new HashMap<String, Object>()).init();
}
内部类(内部类最大的优点就在于它能够非常好的解决多重继承的问题)
为什么使用内部类?
使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无
论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
在我们程序设计中有时候会存在一些使用接口很难解决的问题,这个时候我们可以利用
内部类提供的、可以继承多个具体的或者抽象的类的能力来解决这些程序设计问题。可
以这样说,接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。
特性:
1、内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
2、在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
3、创建内部类对象的时刻并不依赖于外围类对象的创建。
4、内部类是一个独立的实体。
5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。
(1).静态内部类(也是一种懒加载方式)
使用static修饰的内部类我们称之为静态内部类,不过我们更喜欢称之为嵌套内部类。静
态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之
后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没
有这个引用就意味着:
1、 它的创建是不需要依赖于外围类的。
2、 它不能使用任何外围类的非static成员变量和方法。
•只有真正调用getInstance()才会加载静态内部类。加载类时是线程安全的。instance
是static final 型(final可加可不加),保证了只有一个实例存在,而且只能被赋值
一次,从而保证了线程安全。
•兼备了并发高效调用和延迟加载的优势!
public class StaticInnerSingleton {
public StaticInnerSingleton() {}
public static StaticInnerSingleton getInstance() {
return SingletonClassInstance.instance;
}
private static class SingletonClassInstance{
static {
System.out.println("先执行?");
}
private static final StaticInnerSingleton instance = new StaticInnerSingleton();
}
}
(2).接口中使用内部类
public interface Encoder {
Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
class Default implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
if (bodyType == String.class) {
template.body(object.toString());
} else if (bodyType == byte[].class) {
template.body((byte[]) object, null);
} else if (object != null) {
throw new EncodeException(
format("%s is not a type supported by this encoder.", object.getClass()));
}
}
}
}
//使用
Encoder encoder = new Encoder.Default();
创建对象的五种方式
1、使用new关键字
这是最常见也是最简单的创建对象的方式了。通过这种方式,我们可以调用任意的构造函
数(无参的和带参数的)。
2、使用Class类的newInstance方法
我们也可以使用Class类的newInstance方法创建对象。这个newInstance方法调用无参的构造函数创建对象。
我们可以通过下面方式调用newInstance方法创建对象:
3、使用Constructor类的newInstance方法
和Class类的newInstance方法很像, java.lang.reflect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。
4、使用clone方法
无论何时我们调用一个对象的clone方法,jvm就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数。
要使用clone方法,我们需要先实现Cloneable接口并实现其定义的clone方法。
5、使用反序列化
当我们序列化和反序列化一个对象,jvm会给我们创建一个单独的对象。在反序列化时,jvm创建对象并不会调用任何构造函数。
为了反序列化一个对象,我们需要让我们的类实现Serializable接口。
我们从上面的字节码片段可以看到,除了第1个方法,其他4个方法全都转变为invokevirtual(创建对象的直接方法),第一个方法转变为两个调用,new和invokespecial(构造函数调用)。
File和路径的获取
String name = JavaPage.class.getPackage().getName(); // com.ljw09
InputStream inputStream = new FileInputStream(File file);
InputSource inputSource = new InputSource(file.toURI().toURL().toString());
inputSource.getSystemId() // file:/D:/源码/tomcat/apache-tomcat-8.5.39-src/catalina-home/conf/server.xml
xml解析(JDK原生方式)
org.xml.sax.XMLReader
com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl.JAXPSAXParser#parse(org.xml.sax.InputSource)
com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser#parse(org.xml.sax.InputSource)
XMLInputSource xmlInputSource =new XMLInputSource(inputSource.getPublicId(),inputSource.getSystemId(),null);
xmlInputSource.setByteStream(inputSource.getByteStream());
xmlInputSource.setCharacterStream(inputSource.getCharacterStream());
xmlInputSource.setEncoding(inputSource.getEncoding());
parse(xmlInputSource);
com.sun.org.apache.xerces.internal.parsers.XML11Configuration#parse(com.sun.org.apache.xerces.internal.xni.parser.XMLInputSource)
Resource资源文件路径的加载问题
(1) 可以通过类的class文件路径获知当前项目或者编译文件的路径
System.out.println(MemberLotteryController.class.getResource(""));
file:/D:/ideaSpase/julu-other/applet/applet-serviceimpl/target/classes/com/bizvane/appletserviceimpl/controllers/
System.out.println(MemberLotteryController.class.getResource("/"));
file:/D:/ideaSpase/julu-other/applet/applet-serviceimpl/target/classes/
System.out.println(MemberLotteryController.class.getResource("/bootstrap.properties"));
file:/D:/ideaSpase/julu-other/applet/applet-serviceimpl/target/classes/bootstrap.properties
在Class类里面有这样一个方法getResource(String name) 查找带有给定名称的资源获取一个URL
public static void main(String[] args) throws IOException {
//获取当前类class所在的resource路径
System.out.println(TestFilePath.class.getResource("/").getPath());
//获取当前类class所在的路径:
System.out.println(TestFilePath.class.getResource("").getPath());
}
/E:/develop/workspace/selfproject/nutrition/target/test-classes/
/E:/develop/workspace/selfproject/nutrition/target/test-classes/cn/test/
获取这个路径后还要直接拼接资源文件路径,可以直接通过TestFilePath.class.getResource("/request-mapping.properties").getPath()方式获取
(2)、通过new File("")来确定工程目录(非编译运行文件的目录)
public static void main(String[] args) throws IOException {
File f = new File("");
System.out.println("空字符file的Path : " + f.getPath());
System.out.println("空字符file的标准路径CanonicalPath : " + f.getCanonicalPath());
System.out.println("空字符file的绝对路径AbsolutePath : " + f.getAbsolutePath());
}
代码输出结果如下,可以根据资源文件在工程目录对应的路径进行资源路径的拼接,从而完成资源的定位(同时也可以通过直接new File(path)的方式获取文件,其中path是相对于工程目录的路径)
空字符file的Path :
空字符file的正则路径CanonicalPath : E:\develop\workspace\selfproject\nutrition
空字符file的绝对路径AbsolutePath : E:\develop\workspace\selfproject\nutrition
(3)、通过类加载的路径来获取对应的路径信息
System.out.println(MemberLotteryController.class.getClassLoader().getResource(""));
file:/D:/ideaSpase/julu-other/applet/applet-serviceimpl/target/classes/
System.out.println(MemberLotteryController.class.getClassLoader().getResource("/"));
null
System.out.println(MemberLotteryController.class.getClassLoader().getResource("/bootstrap.properties"));
null
public static void main(String[] args) throws IOException {
URL xmlpath = new TestFilePath().getClass().getClassLoader().getResource("/");
URL xmlpath1 = new TestFilePath().getClass().getClassLoader().getResource("");
System.out.println("获取当前类被加载的工程路径:" + xmlpath);
System.out.println("获取当前类被加载的路径:" + xmlpath1);
}
代码输出结果如下,结果说明类加载获取resource时只能用getResource(""),这是因为加载类的路径就默认是在根目录下的,加斜杠反而无法获取路径(将斜杠当成资源名称了)
获取当前类被加载的工程路径:null
获取当前类被加载的路径:file:/E:/develop/workspace/selfproject/nutrition/target/test-classes/
(4)、直接通过java的系统System类获取项目路径
public static void main(String[] args) throws IOException {
System.out.println(System.getProperty("user.dir"));
}
代码输出结果如下,获取的是项目的根目录
E:\develop\workspace\selfproject\nutrition
(5).java获取resource文件
• war包
Thread.currentThread().getContextClassLoader()。
URL l1 =
Thread.currentThread().getContextClassLoader().getResource("readFile/test1.xml");
System.out.println(l1);
URL l2 =
Thread.currentThread().getContextClassLoader().getResource("collection/test2.xml");
System.out.println(l2);
URL l3 = Thread.currentThread().getContextClassLoader().getResource("test3.xml");
String l4=l3.getPath();//加上getPath()则去掉前面的file:
• jar包
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("conf/job_two_one_mapping.txt");
try(Scanner scanner = new Scanner(is)) {
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (Exception e) {
log.error("读取文件数据异常" ,e);
}
Java中的基点有哪些呢?大致总结一下有以下几种:
1)classpath
如果你要找的资源在classpath下,那么通过classpath这个基点是比较合适的,而取得
这个基点方式主要是通过ClassLoader来,具体方法就是 ClassLoader.getResource(String name),而取得ClassLoader的方式很多,比如:
1.Thread.currentThread().getContextClassLoader()
2.clazz.getClassLoader()
3.ClassLoader. getSystemClassLoader()
ClassLoader找resource的实现原理就是先递归在parent classLoader中从所在
classpath里加载resource(最终如何加载JDK未开源),如果所有层级的
classLoader都未找到,则调用findResource方法来找,而这个方法是暴露给自制
classLoader来现实的,因此给了在classpath之外加载resource的机会。
2) 当前用户目录
就是相对于System.getProperty("user.dir" )返回的路径, 对于一般项目,这是项目的根路径。对于JavaEE服务器,这可能是服务器的某个路径。这个并没有统一的规范! 然而, 默认情况下,java.io 包中的类总是根据当前用户目录来分析相对路径名,如new File("xxx"),就是在 System.getProperty("user.dir" )路径下找xxx文件。因此,通过这种方式来定位文件可能会出现移植问题。
3) Web应用程序的根目录
在Web应用程序中,我们一般通过ServletContext.getRealPath("/" )方法得到Web应用程序的根目录的绝对路径。
(6). getResourceAsStream
MemberLotteryController.class.getResourceAsStream("")//获取类所在包的绝对路径
D:\ideaSpase\julu-other\applet\applet-serviceimpl\target\classes\com\bizvane\appletserviceimpl\controllers
MemberLotteryController.class.getResourceAsStream("/")
D:\ideaSpase\julu-other\applet\applet-serviceimpl\target\classes
MemberLotteryController.class.getResourceAsStream("/bootstrap.properties")
D:\ideaSpase\julu-other\applet\applet-serviceimpl\target\classes\bootstrap.properties
什么是桥接方法
什么是桥接方法
桥接方法是 JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法。
我们可以通过Method.isBridge()方法来判断一个方法是否是桥接方法,在字节码中桥接方法会被标记为ACC_BRIDGE和ACC_SYNTHETIC,其中ACC_BRIDGE用于说明这个方法是由编译生成的桥接方法,ACC_SYNTHETIC说明这个方法是由编译器生成,并且不会在源代码中出现。
什么时候会生成桥接方法
就是说一个子类在继承(或实现)一个父类(或接口)的泛型方法时,在子类中明确指定了泛型类型,那么在编译时编译器会自动生成桥接方法(当然还有其他情况会生成桥接方法,这里只是列举了其中一种情况)。如下所示:
SubClass只声明了一个方法,而从字节码可以看到有三个方法,第一个是无参的构造方法(代码中虽然没有明确声明,但是编译器会自动生成),第二个是我们实现的接口中的方法,第三个就是编译器自动生成的桥接方法。桥接方法实际是是调用了实际的泛型方法。
SubClass只声明了一个方法,而从字节码可以看到有三个方法,第一个是无参的构造方法(代码中虽然没有明确声明,但是编译器会自动生成),第二个是我们实现的接口中的方法,第三个就是编译器自动生成的桥接方法。可以看到flags包括了ACC_BRIDGE和ACC_SYNTHETIC,表示是编译器自动生成的方法,参数类型和返回值类型都是Object。再看这个方法的字节码,它把Object类型的参数强制转换成了String类型,再调用在SubClass类中声明的方法,转换过来其实就是:
Method.getDeclaring Class()类的Class对象
java.lang.reflect.Method.getDeclaringClass()方法 //返回表示声明由此Method对象表示的方法的类的Class对象。
public static void main(String[] args) {
Method[] methods = SampleClass.class.getMethods();
Class declaringClass = methods[0].getDeclaringClass();
System.out.println(declaringClass.getName());
}
.Java 8 的 default 方法特性,Java 8 对 Map 增加了不少实用的默认方法
putIfAbsent 方法
其实简单的说:
传统的put方法,只要key存在,value值就会被覆盖,注意put方法返回的是put之前的值,如果无put之前的值返回null
putIfAbsent方法,只有在key不存在或者key为null的时候,value值才会被覆盖 .
putIfAbsent(K key, V value): 根据key匹配Node,如果匹配不到则增加key-value,返回null,如果匹配到Node,如果oldValue不等于null则不进行value覆盖
,返回oldValue
Map<Integer,String> map = new HashMap<Integer,String>();
for(int i=0; i<6; i++){
map.put(i,"val_"+i);
}
map.put(10,null);
//3:V putIfAbsent(K key, V value):根据key匹配Node,如果匹配不到则增加key-value,返回null,如果匹配到Node,如果oldValue不等于null则不进行value覆盖,返回oldValue
System.out.println(map.putIfAbsent(3,"val_66"));//val_3
System.out.println(map.putIfAbsent(10,"val_66"));//null
System.out.println(map.putIfAbsent(11,"val_66"));//null
System.out.println(map.get(3)+"--"+map.get(10)+"--"+map.get(11));//val_3--val_66--val_66
getOrDefault
//1:遍历
map.forEach((key,value) -> System.out.println(key+":"+value));
//2:V getOrDefault(key,defaultValue):获取key值,如果key不存在则用defaultValue
System.out.println("3-->"+map.getOrDefault(3,"val_66"));//3-->val_3
System.out.println("10-->"+map.getOrDefault(10,"val_66"));//10-->null
System.out.println("11-->"+map.getOrDefault(11,"val_66"));//11-->val_66
computeIfAbsent 方法
根据key匹配,参数为key,存在且value不为null,不做修改,为null用返回值作为value,不存在则新增
/** 9:
* computeIfAbsent(K key,Functionsuper K, ? extends V> mappingFunction):
* 根据key做匹配Node,(匹配不到则新建然后重排)
* 如果Node有value,则直接返回oldValue,
* 如果没有value则根据Function接口的apply方法获取value,返回value。
* Function接口的apply的入参为key,调用computeIfAbsent时重写Function接口可以根据key进行逻辑处理,
* apply的返回值即为要存储的value。
*/
System.out.println("----------------------computeIfAbsent------------------------");
map.put(8,null);
System.out.println(map.toString());//{0=val_0, 1=val_1, 2=val_2, 3=val_3, 4=val_4, 5=val_5, 8=null, 10=700000}
System.out.println(map.computeIfAbsent(0,key -> key+"000"));//val_0 -》key值存在,直接返回oldValue
System.out.println(map.computeIfAbsent(7,key -> "value_"+key));//value_7 -》key匹配不到,直接新增,返回值为value
System.out.println(map.computeIfAbsent(8,key -> "88"));//88 -》key匹配到了,value为null,返回值作为value
System.out.println(map.toString());//{0=val_0, 1=val_1, 2=val_2, 3=val_3, 4=val_4, 5=val_5, 7=value_7, 8=88, 10=700000}
computeIfPresent 方法
key,value作为参数,存在,原来的值为null不做操作,否则返回值作为新的value覆盖原来;不存在,不做操作;返回值为null删除该节点
/** 10:
* V computeIfPresent(K key,BiFunction remappingFunction):
* 根据key做匹配,如果匹配不上则返回null,匹配上根据BiFunction的apply方法获取value,返回value。
* BiFunction接口的apply的入参为key、oldValue,调用computeIfPresent时重写Function接口
* 可以根据key和oldValue进行逻辑处理,apply的返回值如果为null则删除该节点,否则即为要存储的value。
*/
map.remove(7);
map.remove(8);
map.replace(10,null);
map.remove(0,"val_0");//value匹配到了删除
map.remove(1,"val_0");//value匹配失败,不会删除
System.out.println(map.toString());//{1=val_1, 2=val_2, 3=val_3, 4=val_4, 5=val_5, 10=null}
System.out.println(map.computeIfPresent(3,(key,value) -> key+":"+value));//3:val_3 -》key存在,根据返回值修改value
System.out.println(map.computeIfPresent(0,(key, value) -> "0000"));//null -》key不存在,返回null,不做任何操作
System.out.println(map.computeIfPresent(1,(key, value) -> null));//null -》key存在,根据返回值修改value
System.out.println(map.computeIfPresent(10,(key,value) -> "val_10"));//null -》oldValue值为null,删除节点
System.out.println(map.toString());//{2=val_2, 3=3:val_3, 4=val_4, 5=val_5, 10=null}
replace
//5:boolean replace(K key, V oldValue, V newValue):根据key匹配node,如果value也相同则使用newValue覆盖返回true,否则返回false
map.put(11,null);
map.replace(3,"3","33");
map.replace(10,"val_66","val_666666");
map.replace(11,null,"val_11");
map.replace(11,null,"val_11");
System.out.println(map.toString());//{0=val_0, 1=val_1, 2=val_2, 3=val_3, 4=val_4, 5=val_5, 10=val_666666, 11=val_11}
replaceAll
/** 6:
* void replaceAll(BiFunction function):调用此方法时重写BiFunction的Object apply(Object o, Object o2)方法,
* 其中o为key,o2为value,根据重写方法逻辑进行重新赋值。
*/
map.replaceAll((key,value) -> {
if(key == 2){
return value+"222";
}
return value;
});
System.out.println(map.toString());//{0=val_0, 1=val_1, 2=val_2222, 3=val_3, 4=val_4, 5=val_5, 10=val_666666, 11=val_11}
compute
根据key做匹配,key,value为参数,匹配到Node做value替换,匹配不到新增node。apply的返回值为null则删除该节点。
/** 7:
* V compute(K key,BiFunction remappingFunction):根据key做匹配,根据BiFunction的apply返回做存储的value。
* 匹配到Node做value替换,匹配不到新增node。apply的返回值如果为null则删除该节点,否则即为要存储的value。
*/
System.out.println("---------------------- compute -----------------------");
System.out.println(map.compute(3,new BiFunction() {
@Override
public Object apply(Object key, Object value) {
return key+":"+value;
}
}));//3:val_3 -》用返回值覆盖原来的值,这里用了java7的编码方式,以下均采用java8的lanbda表达式
System.out.println(map.compute(10,(key,value) -> {return value.split("_")[1];}));//666666 -》用返回值覆盖原来的值
System.out.println(map.compute(6,(key,value) -> null));//null -》返回值为null,则删除该key值
System.out.println(map.toString());//{0=val_0, 1=val_1, 2=val_2, 3=3:val_3, 4=val_4, 5=val_5, 10=666666, 11=val_11}
merge
ldValue,newValue作为为参数,其它功能于compute类似
/** 8:
* merge(K key, V value,BiFunctionsuper V, ? super V, ? extends V> remappingFunction):
* 功能大部分与compute相同,不同之处在于BiFunction中apply的参数,入参为oldValue、value,
* 调用merge时根据两个value进行逻辑处理并返回value。
*/
System.out.println(map.merge(3,"val_3",(value,newValue) -> newValue));//val_3 --》返回值覆盖原来的value
System.out.println(map.merge(10,"33334",(a,b) -> (Integer.valueOf(a)+Integer.valueOf(b))+""));//700000
System.out.println(map.merge(8,"88",(oldValue,newValue) -> oldValue+newValue));//88 -》key不存在则新增
System.out.println(map.merge(11,"11",(old,newValue) -> null));//null -》返回值为null,删除该节点
System.out.println(map.toString());//{0=val_0, 1=val_1, 2=val_2, 3=val_3, 4=val_4, 5=val_5, 8=88, 10=700000}
remove
//4:boolean remove(Object key, Object value):根据key匹配node,如果value也相同则删除
System.out.println(map.size());//8
map.remove(10,"66");
map.remove(11,"val_66");
System.out.println(map.size());//7
System.out.println(map.toString());//{0=val_0, 1=val_1, 2=val_2, 3=val_3, 4=val_4, 5=val_5, 10=val_66}