java知识点整理

199 阅读34分钟
Exception 与 Error区别
1.exception与error都继承throwable

error是会导致程序处于非正常情况下,不可恢复的状态,常见的OutOfMemoryError之类,都是Error的子类。

exception是程序正常运行中可以预料的意外情况,可以通过捕获异常并处理

2.运行异常和一般异常的区别:

运行异常在编译时被强制检查的异常,在方法声明中声明异常。

不受检查异常:不受检查异常通常是在编码中可以避免的逻辑错误,更具需求判断如何处理,不需要在编译期强制要求

3.运行时常见的异常

运行异常RuntimeException是所有不受检查异常的基类。NullPointerException、ClassCastException、NumberFormatException、IndexOutOfBoundsException

final、finally、finalize 有什么不同?
1.定义上的区别

final:是java的声明属性、方法、类的关键字,声明属性表示属性不可变,声明方法表示方法不可被覆盖,类不可以被继承

finally:是捕获异常后最后的执行语句。代表最后总要去执行。

finalize:是Object的一个子类,如果子类中重写了该方法。在垃圾收集器执行的时候会调用被回收对象的此方法。

2.用法上的区别:

final:一个类被final声明定义了。他就不能被继承,因此一个类不能同时被abstract和final修饰,被final修饰的类不能作为父类被继承,但他可以继承其他类。

abstract与final关键字为什么不能同时使用呢?

因为抽象类必须被继承才能使用,而且必须重写所有的抽象方法,但是final不能被修改,所以两个不能同时使用。

如果一个方法被声明为final,那么该方法不能被重写,因此一个方法也不能同时被abstract和final修饰,被final修饰的方法可以被子类继承。

被final声明的变量必须初始化,而后面的引用中只能读取,不可修改。 final变量未被初始化,编译时就会报错 非静态final变量初始化可以直接赋值,代码块赋值、构造函数赋值

finally:finally在异常的处理语句中总会是被执行,只要程序进行了异常处理语句。finally语句就一定会执行。就算try catch中存在return、continue、break这些可以改变程序执行顺序的关键字也不会影响finally的语句执行。finally通常用于关闭IO流,关闭数据库连接。

考察点:try-catch-finally中return的用法

finally块的语句在try或catch中的return语句执行之后返回之前执行,若finally里也有return语句则覆盖try或catch中的return语句直接返回;若finally中没有return则返回try或者catch中的已确定的return值。

**【强制】**不要在finally块中使用return。

说明:finally块中的return返回后方法结束执行,不会再执行try块中的return语句。

finalize

在垃圾回收中,如果被回收的对象重写了finalize方法并且finalize方法中重新建立了改对象与GC ROOT的连接,那么改对象是可以被复活一次的

String、StringBuffer、StringBuilder有什么区别

1.基本区别:

String对象是不可变,String类final类型。不可以被继承。

StringBuffer和StringBuilder对象是可变的。

2.性能区别:

StringBuilder速度最快,其次是StringBuffer,String的执行速度最慢。
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,String对象一旦创建后改对象是不可更改的。后两者的对象是变量可以更改的。

3.安全区别:

String StringBuffer是线程安全的,StringBuilder是线程不安全,(所以如果程序是单线程的使用StringBuilder效率高,如果是多线程使用StringBuffer或者String)

4.相同点:

三者都是处理字符串的,三个类都是被final修饰,因此不可以被继承,StringBuffer与StringBuilder有公共父类

在程序中对字符串+连接时,并不是吧内容动态改变进去,而是每一次都会创建一个新的String对象去放新的字符内容,原来的对象会等着GC回收,所以这也是String为啥相比其他两者执行慢的原因。

从StringBuffer类的源码可以看出,其方法都被同步锁synchronized修饰,因此是线程安全的,因此在多线程编程中操作字符串是推荐使用为什么说StringBuffer比String的执行速度要高,前面说了String的原理,是导致其慢的原因,StringBuffer相比快是因为使用了字符串变量,是可以动态改变的

StringBuilder和StringBuffer的使用方法和原理基本一致的,唯一的区别就是StringBuilder是线程不安全的,执行效率要比StringBuffer高,因此当时单线程的时候推荐使用线程不安全的StringBuilder效率更高一些,如果是多线程推荐使用StringBuffer来保证线程安全

JVM虚拟机

jvm是可以运行java代码的假象计算机,它包括

一套字节码指令集,一个寄存器,一个栈,一个垃圾回收、 堆、jvm他是运行在系统之上的,和硬件没有什么关系。

image.png

运行过程:

源文件通过编译,编译称为.class文件。字节码文件又通过java虚拟机中的解释器,编译成特定机器的机器码 每一个平台的解释器不通,但实现的虚拟机是相同。所以java能够跨平台

java源文件-编译器-字节码文件

字节码文件-jvm-机器码

程序运行时,虚拟机就开始实例化,多个程序就会有多个虚拟机实例。程序退出或者关闭,虚拟机实例也就会消亡。多个虚拟机之间的数据不能共享。

jvm包涵两字子系统和两个组件:

两个子系统:类加载和执行引擎

两个组件:运行时数据区和本地接口

作用:首先通过编译器把java代码转换成字节码,类加载器在把字节码加载到内存中,将其放在运行时数据区的方法区内,而字节码文件只是jvm的一套指令集规范,并不能直接交给底层系统去执行,所以需要特定的解析器执行引擎。将字节码翻译成底层系统的指令,再叫CPU去执行,而这个过程中需要调用其他语言的本地接口来实现整个功能

jvm内存区域

image.png

虚拟机(运行时数据区)内存区域主要分为:计数器,虚拟机栈、本地方法栈,java堆,方法区

堆(运行时常量池):new的对象和数组都会放在堆中

方法区: 加载好的类放在方法区,静态成员

栈:存放局部变量

计数器:存储的是地址描述当前线程接下来要执行的指令在什么地方

堆和栈的区别:

物理地址:

堆的物理地址分配对象是不连续,因此性能慢些。

栈使用的是数据结构中的栈,先进后出的原则,物理地址是连续的。所以性能快。

内存分别:

堆是不连续的,所以分配的内存是在运行期确认的因此大小不固定,一般堆大小远大于栈

栈是连续的,所以分配的内存大小也是确定的。

存放内容:

堆存放的是成员变量,数组,对象实例

栈存放:局部变量,

队列和栈是什么?有什么区别

队列和栈都是用来存储数据的

操作的名称不同:队列的插入称为入队,队列的删除数据为出队,栈的插入为进栈,栈的删除为出栈。

可操作的方式不同。队列是在队尾入队,队头出队,两边都可以操作,栈是进栈和出栈都是在栈顶操作,无法对栈底直接进行从操作。

操作方法不同。队列是先进先出,即队列的修改也是以先进先出的原则进行的。新来的成员总是加入队尾,每次离开的成员总是队列头上。栈为后进先出,即每次删除的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。

java的堆从GC的角度还可以细分为:新生代,老年代。

新生代:用来存放新的对象,一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGc进行垃圾回收。

老年代:主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以MajorGC不会频繁执行。

永久代:指内存的永久保存区域,主要存放Class和Meta的信息,Class在被加载的时候被放入永久区域,GC不会在主程序运行期对永久区域进行清理。

垃圾回收与算法

如何确定是垃圾

引用计数法:java中引用和对象是关联的,操作一个对象必然会用引用进行,一个对象如果没有任何与之相联系的引用,则这个对象的引用计数为0,则说明这个对象不太会用到那就会被认定为垃圾

可达性分析:为了解决引用计数法的循环引用问题,java使用可达性分析方法。通过一系列的"GC roots"对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则该对象是不可达的。不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则可被回收

算法:

1.标记清楚算法:

最基础的算法为标记,清处,标记阶段标记出所有需要回收的对象,清出阶段清出所有被标记的对象所占用的空间。 缺点就是:内存碎片化严重。后续可能发生大对象不能找到可利用空间问题。

2.复制算法:按内存容量将内存划分为等大小的两块,每次只用其中一块,当这一块内存满后,将上存活的对象复制到另一块内存中,把已使用的内存清理掉

优点:实现简单,内存效率高,不易产生碎片。

缺点:但是最大问题是可用内存被压缩到原本的一半。且存活的对象增多的话Copying算法的效率会大大降低。

3.标记整理算法: 标记阶段和标记清除算法一样。标记后不是清理对象,而是将存或的对象移向内存的另一端。然后清除端边界外的对象。

4.分代收集算法:分代收集法目前大部分jvm所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域。GC堆划分为老生代和新生代。老生代的特点就是每次垃圾回收时只有少量的对象被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

4.1新生代与复制算法

每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集.

4.2老年代与标记复制算法

因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.

分区收集算法

分区算法则将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收,根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿。

java四种引用类型

1.强引用:把一个对象赋给一个引用变量,这个变量就是一个强引用,他处于可达状态,他是不肯能被垃圾回收机制回收。即该对象以后永远不会被用到jvm也不会回收,因此强引用是造成java内存泄漏的主要原因之一

2.软引用: 软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时,它不会被回收,当系统内存空间不足时他会被回收,软引用通常在对内存敏感的程序中。

3.弱引用:弱引用需要使用WeakReference类来实现,他比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管jvm的内存空间是否不足够,总会回收该对象占用的内存。

4.虚引用:虚引用需要PhantomReference类来实现,他不能单独使用,必须和引用队列联合使用,虚引用主要作用是跟踪对象被垃圾回收的状态。

JVM类加载机制:

jvm类加载的机制分为五个部分:加载、验证、准备、解析、初始化

加载:这个阶段会在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据入口。

验证:这个阶段主要目的是为了确保Class文件的字节流包涵的信息是否符合当前虚拟机的要求。并且不会危害虚拟机自身的安全。

准备:准备阶段是正式的类变量分配内存并设置类变量的初始值阶段。即在方法区中分配这些变量所使用的内存空间。

解析:解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。

初始化:初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其他操作都由jvm主导。到了初始阶段,才开始真正执行类中定义的java程序代码。

java基础

1.继承

继承就是子类继承父类的特征和行为,使得子类对象具有父类的实例和方法,或子类从父类继承方法,是得子类具有父类相同的行为。

不支持多继承,但支持多重继承

image.png

继承的特性

子类拥有父类非private的方法和属性

子类可以拥有自己的属性和方法,即子类可以对父类进行扩展

子类可以用自己的方法来实现父类的方法

关键字

extends:类的继承是单一继承,也就是说,一个子类只能有用一个父类,所以extends只能继承一个类

implements:可以变相的使用java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口

super 与 this

super:super来实现父类的成员的访问,用来引用当前对象的父类

this: 指向自己的引用

    class Animal { 
        void eat() { 
            System.out.println("animal : eat"); 
        } 
     } 
     
     class Dog extends Animal { 
           void eat() { 
               System.out.println("dog : eat"); 
           } 
           void eatTest() { 
               this.eat(); // this 调用自己的方法 
               super.eat(); // super 调用父类方法 } 
           } 
           
      public class Test { 
          public static void main(String[] args) { 
              Animal a = new Animal(); 
              a.eat(); Dog d = new Dog(); 
              d.eatTest(); 
           } 
      }

构造器 子类是不继承父类的构造器(构造方法或者构造函数)的,它只是调用(隐式或显式)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。

如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器。

java的重写(Override)和重载(Overload)

重写:

1.重写是子类对父类的允许的方法的实现过程进行重新编写,返回值和形参都不能改变,即外壳不变,内容可以重写

2.重写的好处在于子类可以更具需要,定义特定与自己的行为,也就是说子类能够根据需要实现父类的方法。

3.重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常

    class Animal{ 
        public void move(){ 
            System.out.println("动物可以移动");
        } 
    } 
    
    class Dog extends Animal{ 
        public void move(){ 
            System.out.println("狗可以跑和走"); 
        } 
    } 
    
    public class TestDog{ 
        public static void main(String args[]){ 
            Animal a = new Animal(); // Animal 对象 
            Animal b = new Dog(); // Dog 对象 
            a.move();// 执行 Animal 类的方法 
            b.move();//执行 Dog 类的方法 
        } 
    }
 

编译时期看左边,运行时期看右边

尽管 b 属于 Animal 类型,但是它运行的是 Dog 类的 move方法。

这是由于在编译阶段,只是检查参数的引用类型。

然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法。

因此在上面的例子中,之所以能编译成功,是因为 Animal 类中存在 move 方法,然而运行时,运行的是特定对象的方法。

方法的重写规则

1.参数列表与被重写的方法参数列表保持完全相同

2.返回类型与被重写的方法类型可以不相同, 但是必须是父类返回值得派生类。

3.访问的权限不能低于父类中背复写的方法的访问权限更低

4.声明为final的方法不能被重写

5.声明为static的方法不能被重写,但可以再次声明

6.子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法

7.子类和父类不在同一个包中,那么子类只能够重写父类声明为public和protected的非final的方法

8.重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。

9.构造方法不能被重写。(构造方法不可以被重写,因为重写发生在父类和子类之间,要求方法名称相同,而构造方法的名称是和类名相同的,而子类类名不会和父类类名相同,所以不可以被重写。)

10.如果不能继承一个类,则不能重写该类的方法。

重载

重载是在一个类里面,方法名字相同,参数可以不通,返回类型可以相同也可以不通

每一个重载的方法,都必须有一个独一无二的参数类型列表

重载的规则:

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

重载与重写总结:

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • (1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • (2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • (3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

java 多态

多态:是同一个行为具有不通的表现形式和形态的能力

多态:就是同一个接口,使用不通的实例而执行不同的操作。

image.png

多态是对象多种表现形式的体现。

多态存在的三个必要条件

1.继承

2.重写

3.父类的引用指向子类的对象。

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

重写:当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。

要想调用父类中被重写的方法,则必须使用关键字 super

多态的实现方式

方式一:重写

方式二:接口

方式三:抽象类和抽象方法

抽象类

如果一个类没有足够的信息来描述一个具体的对象这样的类就成为抽象类

抽象类不能实例化对象,只能通过继承来使用,除了不能实例化外,类的其他功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。

抽象类表示一种继承关系,一个类只能继承一个抽象类,一个类却可以实现多个接口

声明抽象方法会造成两个结果:

如果一个类包含抽象方法,那么该类必须是抽象类

任何子类都必须重写父类的抽象方法或者使用abstract关键字进行声明,最终必须有子类来实现改抽象方法,否则从最初的父类到最终的子类都不能用实例化对象

抽象类总结:

1.抽象类不能被实力化对象,如果被实力化对象则会报错,编译无法通过,只有抽象类的非抽象子类可以实力化对象

2.具有抽象方法的一定是抽象类,但抽象类中不一定有抽象方法

3.抽象类中的抽象方法只是声明,没有方法体。

4.构造方法,类方法不能声明为抽象方法

5.抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

接口

接口:是抽象方法的集合,接口通常以interface来声明,一个类通过继承接口的方法来继承接口中的抽象方法。

接口并不是类,类是描述对象的属性和方法,而接口只是包涵类要实现的方法

除非实现接口的类是抽象类,否则该类要定义接口的全部方法

接口无法实例化对象,但是可以被实现,一个实现接口的类,必须实现接口内的所有方法,否则必须声明为抽象类。

接口与类的相似点:

一个接口可以有多个方法

接口可以保存在.java的文件中,文件名使用接口名

接口的字节码文件保存在 .class 结尾的文件中

接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:

1.接口不能实例化对象

2.接口没有构造方法

3.接口中的方法必须是抽象方法。

4.接口中不能有成员变量,除了static和final变量

5.接口支持多继承

接口与抽象类的区别:

1.抽象类中可以有非抽象方法,接口中只能有抽象方法

2.抽象类中可以有构造体,接口中不能有

3.抽象类中可以有各种成员变量,接口中只能有public static fianl类型

4.一个类只能继承一个抽象类,一个类却可以实现多个接口。

接口特性

接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。

接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)

接口中的方法都是公有的。

java反射

1.反射的概念

java中反射机制是指在运行状态中,对于任意一个类都能够知道这个类的所有属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为java语言的反射机制

2.反射的应用场合

编译时类型无法获取具体方法,程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为Object,但是程序有需要调用改对象的运行时类型的方法。为了解决这些问题。程序需要在运行时发现对象和类的真实信息。然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息。此时就必须用到反射

3.反射API用来生成jvm的类,接口、或则对象的信息。

3.1 Class类:反射的核心类,可以获取类的属性,方法等信息

3.2 Field类,java.lang.reflec包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值

3.3 Method类:java.lang.reflec包中的类,表示类的方法。他可以用来获取类中的方法信息或者执行方法

3.4 Constructor类:java.lang.reflec 包中的类,表示类的构造方法

4.反射使用步骤(获取class对象,调用对象方法)

4.1.获取想要的操作的类的Class对象,他是反射核心,通过Class对象我们可以任意调用类的方法

4.2.调用Class类中的方法,属于反射的使用阶段。

4.3 使用反射API来操作这些信息

5.获取Class对象的3中方法

5.1调用某个对象的getClass()方法

 Preson p = new Person();
 
 Class clazz = p.getClass();

5.2调用某个类的class属性来获取该类对应的Class对象

   Class clazz = Person.class;
   

5.3使用Class类中的forName()静态方法(最安全最好用)

   Class clazz = Class.forName('类的全路径')

6.如何通过Class类中的方法获取并查看该类中方法和属性

6.1 获取Person类的Class对象

Class clazz = Class.forName('reflection.Person')

6.2 获取Person类的所有方法信息

Method method = clazz.getDeclaredMethods();

for(Method m : method){

    System.out.println(m.toString());

}
 

6.3 获取Person类的所有成员属性信息

Field[] field = clazz.getDeclaredFields();

for(Field f : field){
   
   System.out.println(f.toString());

}

6.4 获取Person类的所有构造方法信息

Constructor[] constructor=clazz.getDeclaredConstructors();

for(Constructor c:constructor){

    System.out.println(c.toString)
}

7.创建对象的两种方法

7.1 Class对象的newInstance()方法来创建该Class对象类的实例,但是这种方法要求该Class对象对应的类有默认的空构造器

7.2 先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建Class对象对应类的实例,通过这种方式可以选定构造方法创造实例。

Class clazz = Class.forName("reflection.Person");

//使用.newInstane 方法创建对象

Person p = (Person)clazz.newInstance();

//获取构造方法并创建对象

Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);

//创建对象并设置属性

Person p1 = (Person) c.newInstance("李四","男",20);

动态代理

在运行时根据我们在java代码中的"指示"动态生成的,相对于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每一个代理类中的方法。运行时自动生成代理对象,缺点是生成代理对象和调用代理方法都要额外花费时间。

动态代理的两种方式:

1.jdk动态代理:基于java反射机制,必须实现了接口的业务类才能用这种办法生成代理对象,新版本也开始Asm机制 java的reflect包下提供了一个proxy类和一个InvocationHandler接口通过这个类和这个接口可以生成jdk动态代理类和动态代理处理对象.动态代理中被代理的对象和代理对象是通过InvocationHandler来完成代理过程的

1.1 创建一个动态代理类步骤

1.1.1 创建一个InvocationHandler对象

InvacationHandler stuHandler = new MyInvocationHandler(stu);

1.1.2使用Proxy的getProxyClass静态方法生成一个动态代理类styProxyClass

Class stuProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(),new Class[] {Person.class});

1.1.3 获得stuProxyClass中一个带InvocationHandler参数的构造器constructor

Constructor<?> constructor = PersonProxy.getConstructor(InvocationHandler.class);

1.1.4通过构造器constructor来创建一个动态实例stuProxy

Person stuProxy = (Person) cons.newInstance(StuHandler);

2.CGLib全称为Code Generation Library,是一个强大的高性能,高质量的代码生成类库,可以在运行期扩展java类与实现java接口,CGLib封装了asm,可以在运行期动态生成新的class,和jdk动态代理相比较,jdk创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过CGLib创建动态代理,

Spring MVC原理

1.客户端http发送请求到DisPatcherServlet(中央处理器),

2.由DisPatcherServlet控制器找到对应handleMapping(处理映射)

3.handleMapping再去调用对应的Controller。

  1. Controller调用业务逻辑代码处理后一个ModeldView

  2. ModelAndView通过DisPatcherServlet找到对应的ViewResoler视图解析器,找到ModelAndView指定的视图

6.处理视图映射并返回模型。

7.视图负责将结果显示到客户端

Spring IOC原理(控制反转,也叫依赖注入)它不是一种技术实现,而是一种设计思想。

Spring 通过一个配置文件描述Bean以及Bean之间的依赖关系,利用java语言的反射功能实例化Bean并建立Bean之间的依赖关系。Spring的IoC容器在完成这些底层工作的基础上,还提供了Bean实例缓存,生命周期管理,Bean实例代理、事件发布、资源装载等高级服务。

在实际项目开发中,我们往往是通过类与类之间的相互协作来完成特定的业务逻辑,这个时候,每个类都要管理与自己有交互的类的引用和依赖,这就使得代码的维护异常困难并且耦合度过高,而IOC的出现正是为了解决这个问题,IOC将类与类的依赖关系写在配置文件中,程序在运行时根据配置文件动态加载依赖的类,降低的类与类之间的耦合度。

它的原理是在applicationContext.xml加入bean标签,在bean标签中通过class属性说明具体类名、通过property标签说明该类的属性名、通过constructor-args说明构造子类参数。

其一切都是反射,当通过applicationContext.getBean("id名称")得到一个类实例时,就是以bean标签的类名、属性名、构造子类的参数为准,通过反射实例对象,唤起对象的set方法设置属性值、通过构造子类的newInstance实例化得到对象。

正因为spring一切都是反射,反射比直接调用的处理速度慢,所以这也是spring的一个问题。我们通过IOC将这些相互依赖的对象的创建、协调工作交给spring去处理,我们只需要关注其自身的业务逻辑就好,这样就由spring容器控制对象如何获取外部资源

Spring aop:面向切面编程

面向切面编程,通过预编译方式和运行期的动态代理实现程序功能的统一维护的一种技术。AOP把软件系统分为了两个部分:核心关注点和横切关注点。业务逻辑的处理叫做核心关注点,其他不会影响业务关系的称为横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似。比如权限认证,日志,事务。AOP的作用在于分离系统中的各种关注点,将和弦关注点和横切关注点分离开。

AOP核心概念

1.切面:类是对物体特征抽象,切面就是对横切关注点的抽象

2.横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。

3.连接点:被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到方法,实际上连接点还可以使字段或者构造器。

4.切入点:对连接点进行拦截的定义

5.通知:所谓的通知指的就是只拦截到连接点之后要执行的代码,通知分为前置,后置、异常、最终、环绕通知五类。

6.目标对象:代理的目标对象

7.织入:将切面应用到目标对象并导致代理对象创建的过程。

8.引入:在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。

aop的两种代理方式:

Spring 提供两种方式生成代理对象,一种是通过jdk动态代理,一种是Cglib,默认的策略是如果目标类是接口,则使用jdk动态代理。否则使用cglib

JAVA多线程并发

线程与进程

1.1进程是一段正在执行的程序,是资源分配的基本单位,而线程是cpu的调度基本单位。

1.2进程间响度独立的进程,进程之间不能共享资源,一个进程至少有一个线程,同一进程的各线程的各个线程可以共享整个进程的资源。

1.3线程的创建和切换开销比进程小

线程的创建与实现

1.继承Thread类

Thread类本质是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法 start()方法是一个native方法,他将启动一个新线程,并执行run()方法。

public class MyThread extends Thread{
    public void run(){
        System.out.println("MyThread.run()");
    }
}
MyThread mythread = new Mythread();
myThread1.start();

2.实现Runnable接口

如果自己的类已经继承了另一个类,就无法直接继承thread,此时可以实现Runable接口.

    public class MyThread extends OtherClass implements Runnable{
        public void run(){
            System.out.println("MyThread.run()")
        }
    }
    //启动MyThread,需要实例化一个Thread,并传入自己的MyThread实例中
    MyThread myThread=new MyThread();
    Thread thread = new Thread(myThread)
    thread.start()
    //事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run()
    public void run(){
        if(target!=null){
            target.run();
        }
    }
    

3.Callable、Future有返回值得线程

有返回值得任务必须实现Callable接口,无返回值得任务必须实现Runnable接口。执行Callable任务后,可以获取一个Futrue的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,在结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。

    //创建一个线程池
    ExecutorService pool = Executors.newFixedThreadPool(taskSize);
    //创建多个有返回值得任务
    List <Future> list = new ArrayList<Future>();
    for(int i=0;i<taskSize;i++){
        Callable c = new MyCallable(i+" ");
        //执行任务并获取Future对象
        Future f = pool.submit(c);
        list.add(f);
    }
    //关闭线程池
    pool.shutdown();
    //获取所有并发任务的运行结果
    for(Future f:list){
        //从Future对象上获取任务的返回值,并输出到控制台
        System.out.println("res:"+f.get().toString());
    }
    

4.线程池的方式

线程和数据库连接这种资源都是非常宝贵的资源,如果每次创建,不需的时候销毁,是非常浪费资源,我们可以使用缓存策略,就是是线程池

    //创建线程池
    ExecutorService thredPool = Executors.newFixedThreadPool(10);
    while(true){
        threadPool.execute(new Runnable(){
            //提交多个线程任务,并执行
            @Override
            public void run(){
                System.out.println(Thread.currentThread().getName()+"is running..");
                try{
                    Thread.sleep(3000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        })
    }

5. 4种线程池

Java里面线程池的顶级接口是Executor,但是严格意义上将Executor并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是ExecutorService.

5.1 newCachedThreadPool(可缓存线程池)

特点:无线大,如果线程池中没有可以用线程就会自动创建,有的话自动利用起来。

   public class CachedThreadPool{
       public static void main(String[] args){
           //Executor 调度器
           //ExecutorService 用于管理线程池
           //创建一个可缓存在线程池
           ExecutorService executorService = Exectors.newCachedThreadPool();
           for(inti= 1;i<1000;i++){
               exectorService.execute(new Runnable(){
                   @Override
                   public void run(){
                       System.out.println(Thread.currentThread().getName()+"is running..");
                  
                   }
               });
           }
           try{
                Thread.sleep(3000);
           }catch(InterruptedException e){
                e.printStackTrace();
           }
           executorService.shutdown();
       }
   } 

5.2 newFixedThreadPool(定长线程池)

特点是:固定线程总数,空闲线程用于执行任务。如果线程都在执行任务后续任务则处于等待状态,在线程池中的线程执行任务后再执行后续任务。 如果线程处于等待状态,备选的等待算法默认为FIFO(先进先出),还有LIFO(后进先出)

    public class FixedThreadPool{
        public static void main(String[] args){
           //Executor 调度器
           //ExecutorService 用于管理线程池
           //创建一个定长线程池
           //定长线程池的特点:固定线程总数,空闲线程用于执行任务,如果线程都在执行任务测任务处于等                //待状态,
           ExecutorService executorService = Exectors.newFixedThreadPool(6);
           for(inti= 1;i<1000;i++){
               exectorService.execute(new Runnable(){
                   @Override
                   public void run(){
                       System.out.println(Thread.currentThread().getName()+"is running..");
                  
                   }
               });
           }
           try{
                Thread.sleep(3000);
           }catch(InterruptedException e){
                e.printStackTrace();
           }
           //shutdown()代表关闭线程池
           //shutdownNow()立即终止线程池,不等待线程执行完毕,不推荐使用
           executorService.shutdown();
        }
    }
    

5.3 newSingleThreadExecutor(单线程池)

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

      public class SingleThreadPool{
          public static void main(String[] args){
           //Executor 调度器
           //ExecutorService 用于管理线程池
           //创建一个定长线程池
           //定长线程池的特点:固定线程总数,空闲线程用于执行任务,如果线程都在执行任务测任务处于等                //待状态,
           ExecutorService executorService = Exectors.newSingleThreadPool(); 
           for(inti= 1;i<1000;i++){
               exectorService.execute(new Runnable(){
                   @Override
                   public void run(){
                       System.out.println(Thread.currentThread().getName()+"is running..");
                  
                   }
               });
           }
           try{
                Thread.sleep(3000);
           }catch(InterruptedException e){
                e.printStackTrace();
           }
           //shutdown()代表关闭线程池
           //shutdownNow()立即终止线程池,不等待线程执行完毕,不推荐使用
           executorService.shutdown();
          }
      }

5.4 newScheduledThreadPool-调度线程池

特点:可以根据设定的时间间隔执行任务。
schedule()设定的时间间隔执行一次;
scheduleAtFixedRate()设定的时间间隔重复执行。

    public class ScheduleThreadPool {
        public static void main(String[] args){
            ScheduledExecutorService scheduleExecutorService =             
            Executors.newScheduledThreadPool(5);
            schedleExecutorService.scheduleAtFixedRate(new Runnable(){
                @Override
                public void run(){
                    System.out.println(new Date() + "延迟1秒执行,每3秒执行一次")
                }
            },1,3TimeUnit.SECONDS);
        }
    }

线程生命周期

image.png

新建(new

当程序使用new关键字创建一个新的线程之后,该线程就处理新建状态。

就绪(RUNNABLE)

当线程对象调用start()方法之后,该线程处于就绪状态。等待调度运行

运行(RUNNING)

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态

阻塞(BLOCKED)

阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行,直到线程进入(runnable)状态,才有机会再次获得cpu timeslice转到运行(running)状态。

阻塞的情况分为三种

  1. 等待阻塞 :

运行(running)的线程执行o.wait()方法,jvm会把该线程放入等待队列(waitting queue)中

  1. 同步阻塞 :

运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中

  1. 其他阻塞 :

运行(running)线程执行Thread.sleep或t.join()方法,或者发出了I/O请求时,jvm会把该线程设置为阻塞状态。

当sleep()状态超时,join()等待线程终止或者超时,或者I/o处理完成时,线程重新转入可运行状态。

线程死亡的三种方式:

  1. 线程在正常情况下正常消亡

  2. 调用stop()方法来结束线程容易造成死锁,不建议使用。

  3. 异常停止,线程抛出异常或者错误

终止线程的4种方式

1.线程自动结束

2.使用interrupt()方法来中断线程

2.1线程处于阻塞状态:当使用interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法排除这

个异常通过代码捕获该异常后然后break跳出循环状态,从而结束线程

2.2 线程未处于阻塞状态,使用isInterrupt()判断线程的中断表示来退出循环。当使用interrupt()方法时,

中断标志就会转换为true。

public class ThreadSafe extends Thread {
    public void run(){
        while(!isInterrupted()){
            try{
                Thread.sleep(5*1000);
            }catch(InterruptedException e){
                e.printStackTrace();
                break;
            }
        }
    }
}

3.使用退出标志退出线程

使用一个变量来控制循环,例如:最直接的方法就是设一个boolean类型的标志,并通过设置这个标志true或false来控制while循环是否退出。

定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。

sleep与wait区别

1.sleep属于Thread类方法,wait()属于object类中

2.sleep不需要唤醒,时间结束后悔自行恢复运行状态,使用wait()方法会让线程放弃对象锁,进入等待此对象的等待锁定池中。只有通过notify()去唤醒

3.sleep()不会释放对象锁

start与run区别 1.start()是启动线程,真正的实现了多线程运行,这是无需等待run方法代码执行完毕,可以直接继续执行下面的代码。

2.通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。

3.方法run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码。 Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

线程池的原理

线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后再线程创建后启动这些任务,如果线程最大数量超出数量的线程排队等候,等其他线程执行完毕后,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数,管理线程

线程池的工作过程

1.线程池刚创建时,里面没有线程,任务队列是作为参数传进来的,不过,就算队列里面有任务,线程池也不会马上执行他们。

2.当调用execute()方法添加一个任务时,线程池会做如下判断:

2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

2.3如果这时候队列满了,而且正在运行的线程数量小于maxmumPoolSize,那么还是要创建非核心线程立刻运行这个 任务

2.4如果队列满了,而且正在运行的线程数量大于或等于maxmumPoolSize,那么线程池会排除异常

3.当一个线程完成任务时,它会从队列中取下一个任务来执行

4.当一个线程无事可做,超过一定时间时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉,所以线程池的所有任务完成后,它最终会受到corePoolSize的大小。

image.png

java锁

乐观锁:

乐观锁是一种乐观思想,任务读多写少。遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新)如果失败则重复读-比较-写操作。 比较当前值跟传入值是否一样,一样则更新否则失败

实例: 般是说在数据表中加上⼀个数据库版本号version字段,在表述数据被修改的次数当数据被修改时,它的version 值会加1。 如: 当然线程A需要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

悲观锁:

悲观锁就是悲观思想,认为写的多,遇到并发写的可能性比较高,每次去拿数据的时候后认为别人会修改,所以每次读数据的时候都会加上锁。这样被人想读写这个数据的时候就会block(阻塞)直到拿到锁。java中的悲观锁就是Synchronized.

自旋锁:

如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态。它们只需要等一等。等持有锁的线程释放锁后立即获取锁,这样就避免了用户线程和内核的切换消耗。

Synchronized 同步锁

synchronized他可以把任意一个非null的对象当作锁。

作用范围:

1.作用于方法时,锁住的是对象的实例(this);

2.当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen,永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有该调用该方法的线程;

3.synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块,他有多个队列,当线程一起访问某个对象监视器会将这些线程存储在不同的容器中。

volatile关键字的作用

volatile变量,用来确保将变量更新操作通知到其他线程,volatile变量具备两种特性,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方。因此在读取volatile类型变量时总会返回最新写入的值。

集合

1.collection:Collection集合是List、Set、Queue的最基本接口

2.Iterator:迭代器,可以通过迭代器遍历集合中的数据

3.Map:是映射表的基础接口

image.png

List接口

list接口是有序的Collection,使用此接口能够精确的控制每一个元素插入的位置,能够通过索引,类似于数组下标的方式来访问list中的元素,第一个元素的索引为0,而且允许有相同的元素。

list一共有三个实现类:ArrayList,Vector和LinkedList

ArrayList

内部通过数组实现。允许元素进行快速随机访问。数组的缺点是每一个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就将已经有数组复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动。代价比较高,因此它适合随机查询和遍历,不适合插入和删除

Vector

Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。

LinkList

LinkedList是用链表结构存储数据的。很适合数据的动态插入和删除,随机访问和遍历速度比较慢,另外,他还提供了List接口没有定义的方法,专门用于操作表头和表尾元素,可以当做堆栈、队列和双向队列使用。因为数组内存的连续性,操作起来他的时间复杂度为O(n),链表本身就不是连续的,所以他插入和删除数据的时候特别快,只考虑相邻结点的指针改变,所以对应的时间复杂度是O(1)

Set接口

Set注重独一无二的性质,该体系结合用于存储无序元素(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象hashCode值(java是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等,就必须覆盖Object的hashCode方法和equals方法。

HashSet

哈希表边存放的是哈希值,hashSet存储元素的顺序并不是按照存入时的顺序,而是按照哈希值来存储的所以取数据也是按照哈希值取得,元素的哈希值是通过元素的hashcode方法来获取的,HashSet首先会判断两个元素的哈希值,如果哈希值一样,就会比较equlas方法,如果equls结果为true,hashSet就视为同一个元素,如果equals为false,就不是同一个元素。

哈希值相同的equals为false的元素时怎么存储呢?

就是在同样的哈希值下顺延。也就是哈希一样的存一列。

hashCode值不相同的情况

image.png

hashCode值相同,但equals不相同的情况

image.png

HashSet通过hashCode值来确定元素在内存中的位置。一个hashCode位置上可以存放多个元素。

TreeSet

1.TreeSet()是使用二叉树的原理对新add()的对象按照指定的顺序排序,每增加一个对象都会进行排序,将对象插入到二叉树指定的位置。

2.Integer和String对象都可以通过进行默认的TreeSet排序,而自定义的对象是不可以。自定义的对象是必须实现Comparable接口,复写compareTo()方法

3.在覆写compare()函数时,要返回相应的值才能使TreeSet按照一定的规则来排序。

4.比较此对象与指定对象的顺序,如果该对象小于、等于或者大于指定的对象,则分别返回负数。零或者正整数。

TreeSet排序

TreeSet进行排序的方法的方法有两种,分别是

一种是类实现Comparable<>方法,[重写]compareTo(Object o)方法

public class Demo1 { public static void main(String[] args) {

    Student s1 = new Student("张三",25);
    Student s2 = new Student("李四",18);
    Student s3 = new Student("王五",20);

    //Student类实现Comparable接口,TreeSet就可以对学生进行自然排序
    TreeSet<Student> ts = new TreeSet<>();
    ts.add(s1);
    ts.add(s2);
    ts.add(s3);

    for(Student s : ts){
        System.out.println(s);
    }

}

}

//学生类实现Comparable接口,TreeSet就可以对学生进行自然排序
public class Student implements Comparable<Student>{

    private String name;
    private  int age;

    //实现元素排序的规则
    /*
        根据返回值排序
            返回的负数,认为当前要存入的元素比较小,存左边
            返回的正数,认为当前要存入的元素比较大,存右边
            返回的0,认为当前要存入的元素已经存在,不存入
     */
    @Override
    public int compareTo(Student o) {
        //按照年龄升序排序 (从小到大)
        return this.age - o.age;

        //按照年龄降序排序(从大到小)
        //return o.age - this.age;
    }

底层原理是:

CompareTo(Student o){return = this.age - o.age}:根据类型进行比较

例如三个学生对象,第一个存进去,第二个跟第一个比较,如果比较后负数,则在左边存储,第三个先跟第一个比较,如果为负数则存进左边,如果为正数,然后就跟第二个对象比较,也是负数存在左边,正数存在右边。

返回的是负数:认为当前要存入的元素比较小,存黑红树左边

返回的是正数:认为当前要存入的元素比较大,存黑红树右边

返回的是0:认为当前要存入的元素已经存在,不存入

另一种使用Comparetor比较器

方式:

   TreeSet<Student> a = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o2.getAge()-o1.getAge();
            }

    });

当属性不是int,而是double时,不能强制,因为会有精度问题,应该用if条件判断这个数到底是正数负数还是0

原理跟CompareTo方法是一样的,都是根据返回值来计算

LinkHashSet

对于LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。LinkedHashSet底层使用LinkedHashMap来保存所有元素。它继承与HashSet,其所有的方法操作上又与HashSet相同,,因此LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap来实现,在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。

Map

1.HashMap(数组+链表+红黑二叉树)

hashMap是根据键的hashCode值存储数据,大多数情况下可以直接定位到他的值,因而很快确定访问速度。但遍历顺序是不确定的。

hashMap允许一条记录的键为null,允许多条记录的值为null。

hashMap非线程安全,即任一时刻可以有多个线程同时写hashMap可能会导致数据不一样。如果需要满足线程安全,可以用Collections的synchronizeMap方法使HashMap具有线程安全的能力,或使用ConcurrentHashMap。

java7 HashMap结构(数组+链表)

image.png

大方向上,HashMap里面是一个数组,数组中的每一个元素是一个单向链表。上图中的每一个绿色的实体就是一个Entry实例,一个entry实例包括四个属性:key,value,hash值和单向链表的next(后指针)

java8实现:(数组+链表+红黑二叉树)

image.png

java7中HashMap的介绍,更具hash值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度。为O(n)。所以在java8以后,当链表中的元素超过8个以后。会将链表转为红黑二叉树。这个时候查询的时间可以降低时间复杂度为O(logN)。

TreeMap(可排序)

底层为二叉树的数据结构。键不可以重复,值可以重复

TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键的升序排序,也可以指定排序比较器。当Iterator遍历TreeMap时,得到的记录是拍过序的 public class SortDemo {

public static void main(String[] args) {
    System.out.println("---------------- 默认 排序结果-----------------");
    createDefaultSortTreeMap();
    System.out.println("---------------- 自定义 排序结果-----------------");
    createDefinitionSortTreeMap();
}

public static void createDefaultSortTreeMap() {
    TreeMap<String, String> map = new TreeMap<String, String>();
    
    init(map);
    print(map);
}

public static void createDefinitionSortTreeMap() {
    
    TreeMap<String, String> map = new TreeMap<String, String>(new Comparator<String>() {

        @Override
        public int compare(String o1, String o2) {
                return o2.compareTo(o1);
        }
        
    });
    
    init(map);
    print(map);
}

public static void init(Map<String, String> map) {
    map.put("c", "1");
    map.put("a", "1");
    map.put("bb", "1");
    map.put("b", "1");
}

public static void print(Map<String, String> map) {
    Iterator<Entry<String, String>> it = map.entrySet().iterator();
    while(it.hasNext()) {
        Entry<String, String> entry = it.next();
        System.out.println(entry.getKey() + " : " + entry.getValue());
    }
}

结果:
---------------- 默认 排序结果-----------------

a : 1
b : 1
bb : 1
c : 1
---------------- 自定义 排序结果-----------------
c : 1
bb : 1
b : 1
a : 1

LinkHashMap LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。