Java基础复习计划(二)

散碎知识点

  • 通过 HttpServletRequest. getParameter() 获取的参数编码格式由浏览器决定。
    浏览器根据 html 中指定的编码格式进行编码,tomcat 根据指定的格式进行解码,tomcat 默认解码是 ISO-8859-1.
    get 请求使用 new String(username.getBytes("ISO-8859-1"), "UTF-8"); 解决乱码;
    post 请求使用 request.setCharacterEncoding("utf-8");

  • for(;;)while(true) 都是无条件循环,使用 javac 编译后他们两个是一样的字节码.

  • final 只是指向不变,但是指向的值有可能变,所以依然不是线程安全

  • 包装类的 equals() 方法不处理数据转型,也就是说用 Integer 的 equals 比较 Long 类型,即使值相同也不返回 true

  • 调用 Object.wait() 会释放锁,获得执行权后会再尝试获取锁。

  • null 可以被强制类型转换成任意类型(不是任意类型对象),于是可以通过它来执行静态方法。

  • 字符串常量池在 Java6 之前存放在方法区,而 Java7 中又把常量池移到了堆中(运行时常量池在方法区)
    字符串本身是一个对象啊,对象在堆中,常量池里面放的只是一些引用,这些引用指向了堆中的具体的对象。

  • private 和 protected 修饰符不能修饰类。
    一个类如果是私有的,其他都不能访问,那么它只能自己玩自己,没有意义。
    一个类如果是受保护的,那么继承后可以访问,既然未继承之前是不可见的,那么也就无法进行继承,这样就显得毫无意义,不如直接用 default。

  • 普通的初始化块 用于初始化非静态的属性;静态初始化块用于初始化静态属性,静态属性只有一份,也就是只会执行一次
class Test{
int i = 3;
// 等价于下面(会被翻译成下面的形式)
int i;
{
  i = 3;
}
// 静态变量同理
static int a = 2;
// 等价于
static int a;
static {
  a = 2;
}
// 一个例子
static {
  x = 3;
  System.out.println(x);
}
static int x = 2;
}

可以看到初始化操作时,还是会翻译成代码块的形式依次执行;在最后的一个例子中,打印语句会报错,JVM 不允许完全初始化之前使用变量,然后就是变量定义的初始化优先(语法优先),所以即使把 x 的定义放在下面也是可以编译运行的,顺序就成了:先执行定义语句 static int x; 然后依次开始执行 x = 3;x = 2; 最终 x 的值就是 2 了。

构造方法

是在创建对象的时候需要调用的方法 => 它是收尾的步骤。

注意:并不是用构造方法来创建对象,创建对象的过程是很复杂的,构造方法会在收尾时调用。

构造方法的修饰符默认和类名一致,构造方法的首行默认就是 super() 也就是调用父类的无参构造方法。

构造方法的首行 还可能出现 this() ,代表在执行当前的构造方法之前先去执行本类的其他构造方法。

由于 super() 和 this() 都必须出现在首行,所以它们无法同时存在,并且 super() 是默认值

构造方法是不可能覆盖的,因为它不会被继承,所以覆盖无从谈起。

参数传递

总结一下就是下面的两条总则:

  • 基本数据类型传参赋值的时候 其实就是把值直接复制了一份
  • 引用数据类型传引用的值 而引用的值就是一个内存指向的地址

然后来看下面的代码:

public class TestArgs3{
  public static void main(String[] args){
    String a = new String("O");
    String b = new String("K");
    change(a,b);
    System.out.println(a);//?
    System.out.println(b);//?
  }
  public static void change(String x,String y){
    // 相当于是 String x = a;String y = b;
    String temp = x;
    x = y;
    y = temp;
  }
}

虽然 String 是引用数据类型,打印结果依然是 “OK”,a 和 b 的内存地址确实是赋给 x 和 y了,然后执行的交换操作是交换的局部变量!! 也就是 x 和 y 的地址确实改变了,但是和 a 、b 没有半毛钱关系。

然后来看个完整版:

public class TestArgsFinal{
  public static void main(String[] args){
    int num = 2;
    change(num);
    System.out.println(num);//2
    Int ok = new Int(num);
    change(ok);
    System.out.println(ok.i);//3
    changeRef(ok);
    System.out.println(ok.i);//3
  }
  public static void change(int x){
    x = 5;
  }
  public static void change(Int ia){
    ia.i = 3;
  }
  public static void changeRef(Int ia){
    ia = new Int(7);
  }
}
class Int{
  int i;
  public Int(int i){
    this.i = i;
  }
}

也就是说,只有通过 . 属性的方式修改的,方法结束后才会保留,通过 new 的当方法结束,也就随之消亡了。

字符串

在给字符串赋值时,通过 "" 的话涉及到字符串常量池,new 不会涉及常量池。

当使用 "" 进行赋值时,内容会被收录到常量池当中,而当再次出现双引号直接赋值的时候,会进行常量池的过滤查找,如果已经出现过,则不会再分配新的空间,而直接指向原有(已经存在的)空间。

StringBuffer/StringBuilder 常用方法:

  • append()
  • insert()
    在指定的下标插入内容,使用的其实是 System.arraycopy() 来移动“数组”的。
  • reverse()

    反转整个字符串

SB 中默认设置 16 个缓冲区,new 的时候可以手动进行指定。

String 其实就是个 char[] ,与 c 不同的是,末尾没有 \0 标识。

抽象类

重要的几个特点:

  1. 抽象类中可以构造方法
  2. 抽象类中可以存在普通属性,方法,静态属性和方法。
  3. 抽象类中可以 存在/不存在 抽象方法。
  4. 如果一个类中有一个抽象方法,那么当前类一定是抽象类;抽象类中不一定有抽象方法。
  5. 抽象类中的抽象方法,需要有子类实现,如果子类不实现,则子类也需要定义为抽象的。
  6. 抽象类不能被实例化,抽象类和抽象方法必须被 abstract 修饰

关键字使用注意:

抽象类中的抽象方法(其前有 abstract 修饰)不能用 private、static、synchronized、native 访问修饰符修饰。

接口

接口的几个重要特点:

  1. 在接口中只有方法的声明,没有方法体(JDK1.8 以前)。
  2. 在接口中只有常量,因为定义的变量,在编译的时候都会默认加上 public static final
  3. 在接口中的方法,永远都被public abstract来修饰。
  4. 接口中没有构造方法,也不能实例化接口的对象。(所以接口不能继承类)
  5. 接口可以实现多继承
  6. 接口中定义的方法都需要有实现类来实现,如果实现类不能实现接口中的所有方法则实现类定义为抽象类。
  7. 接口可以继承接口,用 extends

JDK8.0 开始 接口当中有两种情况是可能出现方法体的:

  1. static 修饰的静态方法
  2. default 修饰的默认方法

Java 中的四大金刚:class、interface、enum、annotation

他们是同一级别的,编译都会产生 class 文件,后两个是 JDK6.0 出现的吧

类加载相关

关于这方面,我找到了一篇写的很棒的文章,备份了一份在:Github

类的加载顺序可以简单归纳为(具有继承关系的情况下):

  1. 父类静态代码块 (包括静态初始化块,静态属性,但不包括静态方法)
  2. 子类静态代码块 (包括静态初始化块,静态属性,但不包括静态方法 )
  3. 父类非静态代码块 ( 包括非静态初始化块,非静态属性 )
  4. 父类构造函数
  5. 子类非静态代码块 ( 包括非静态初始化块,非静态属性 )
  6. 子类构造函数

其中:类中静态块按照声明顺序执行(因为是同级的),并且 1 和 2 不需要调用 new 类实例的时候就执行了(意思就是在类加载到方法区的时候执行的)

然后再来关注一下方法覆盖的问题,看下面的代码:

public class Base{
  private String baseName = "base";
  public Base(){
    callName();
  }

  public void callName(){
    System.out.println(baseName);
  }

  static class Sub extends Base{
    private String baseName = "sub";
    public void callName(){
      System.out.println(baseName) ;
    }
  }
  
  public static void main(String[] args){
    Base b = new Sub();
  }
}

最后打印的是 null;对于这种动态的多态,编译时表现为 Base 类特性,运行时表现为 Sub 类特性。

当子类覆盖了父类的方法后,意思是父类的方法已经被重写,父类初始化调用的方法为子类实现的方法,子类实现的方法中调用的 baseName 为子类中的私有属性。

这时候才执行到第四个步骤,子类非静态代码块和初始化步骤还没有到,子类中的 baseName 还没有被初始化。所以此时 baseName 为空,所以为 null。

集合框架

JavaSE 中最重要的知识点之一,Java Collections Framework。

Java当中的集合当中只能存放对象在内存当中的地址,也就是说基本数据类型无法放入,中间会有自动装箱/拆箱的过程,关于包装类有两个特殊的(其他的都是首字母大写而已)就是 Character 和 Integer。

PS: Integer.parseInt() 是将字符串转换为 int 类型;Integer.valueOf() 是转换为 Integer 类型。

自动装箱默认就是调用的 valueOf ,并且默认会缓存 -128 ~ 127 的数,其他的包装类也类似。

自动拆箱默认调用的是 intValue、floatValue 等等。

总的来说,集合框架可分为两大类,Collection(单值类型集合)Map(键值对集合,主键对象唯一)

Collection 又分为 List(有序,不唯一,可添加空值)Set(无序,唯一);Map 下还有 SortedMap(主键对象唯一且有序)

Set 想要有序就用 SortedSet 咯,有序唯一。

Collection 系列通用的常用方法:

  • addAll
    添加另一个集合

  • retainAll
    求交集

  • removeAll
    批量“删除”,如果传入的集合包含(contains)调用集合(挨个遍历)就“删除”。

List

首先看最出名的 ArrayList 的基本使用。

List<Integer> list = new ArrayList<>();

JDK5+ 后支持泛型,JDK7+ 后支持泛型自动推断;添加元素除了使用 add 还以可以使用 Collections.addAll(list,a1,a2,a3) 后面是一个可变参数,就是相当于一个数组(重载时,数组和可变参数视为一样的),可变参数也是 JDK5 加入的。

遍历 List 一般有四种方法:

  1. fori + get()

  2. foreach
    内部还是使用的迭代器实现,所以在迭代过程中不允许增加、删除元素

  3. Iterator
    建议的书写方式:
for(Iterator<?> car = list.iterator(); car.hasNext(); ){
   System.out.println(car.next());
   // 使用迭代器删除元素是安全的
   // car.remove();  // 删除当前指针所处位置的元素
}

使用 for 的目的是可以控制 iterator 的生命周期

  1. lambda表达式
    JDK8 的新特性,用起来确实爽:

    list.forEach(System.out::println);

    使用到了函数式编程,可以往里传一个函数(方法),方法名与类名使用 :: 分割。

然后下面来看删除,remove 方法有两种重载,可以按照下标来删除,也可以按照对象来删除;这里需要注意的是按照对象删除的时候会用要删除的对象的 equals 和集合里的元素进行比较,如果返回 true 就进行删除,所以有时可能需要进行重写 equals 方法。

PS:当删除一个元素后,后面的元素会向前移动,fori 操作时需要注意一下;并且因为要移动所以效率不高,不如从最后删除的快。

针对 ArrayList 有两个常用的专有方法,因为它内部使用的是数组结构:

ArrayList list = new ArrayList(2);
//扩容,扩容到指定的大小
list.ensureCapacity(20);
//缩容,将空余的空间给释放  
list.trimToSize();

建议如果你知道 ArrayList 具体装多少数,那么就在 new 的时候直接指定,避免扩容带来的耗时,也可以使用两个专有方法进行扩容和缩容。


下面就来详细说说 ArrayList,从名字就可以看出它是采用数组实现的,下面一些代码参考:

class MyList{
  private Object[] data;
  private int size;

  public MyList(int x){
    data = new Object[x];
  }
  // 默认空间为 10
  public MyList(){
    this(10);
  }

  public int size(){
    return size;
  }

  public Object get(int x){
    return data[x];
  }

  //添加元素的方法 add()
  public void add(Object obj){
    if(size == data.length){
      Object[] temp = new Object[size*3/2+1];
      System.arraycopy(data,0,temp,0,size);
      data = temp;
    }
    data[size++] = obj;
  }

  //删除元素的方法1 remove(int)
  public void remove(int x){
    // TODO 需要检查是否越界
    System.arraycopy(data,x+1,data,x,size-- - x-1);
  }

  //删除元素的方法2 remove(Object)
  public void remove(Object obj){
    for(int i = 0;i<size;i++){
      if(obj.equals(data[i])){
        remove(i);
        return;
      }
    }
  }
}

可以看出内部使用一个计数器来实现记录数组里已经装了多少元素,add 的时候就加一,remove 的时候就减一,add 时还要判断是不是满了,满了就扩容。

至于每次扩容多少呢,在 JDK6- 是 x*3/2+1 也就是 1.5 倍,后面的 +1 是为了防止 new 的时候就创建了一个大小为 1 的数。

到了 JDK7+,变成了 x+(x>>1) ,还是 1.5 ,变成了位操作更加高效,也不再考虑 1 的情况,为了这一种情况让所有的都 +1 不同值得,前面加个判断就行了,如果是 1 直接变为 2。

然后还有一个就是 Iterator,我们知道 Iterator 为了防止并发错误,在迭代的过程是不允许进行操作的,那么它是如何做到的?

就是 List 中有个 int 类型的值 modCount ,它会记录 List 一些进行的操作,每次操作就将其加一,使用 list.iterator() 的时候会将其拷贝一份到迭代器中,然后进行 next 操作的时候如果发现拷贝的值和 List 中的值不一样,就意味着发生了变化,那么就会抛出异常了。

ArrayList和LinkedList

ArrayList 底层是采用数组实现,数组结构的最大优势就是连续存储,为了保证连续存储所以它的添加和删除都复杂些(有时需要涉及扩容),优势在于查找、遍历和随机访问效率较高。

LinkedList 底层采用链表实现,优势在于添加和删除效率较高,但是查找和随机访问效率较低,所以应该尽量回避它的 get 方法,遍历的话使用 foreach 或者 Iterator 效率会较高。

ArrayList和Vector

  • 同步特性不同
    也就是说 ArrayList 是线程不安全的,运行多个线程同时操作;
    Vector 是线程安全的,同一时间只允许一个线程操作。

  • 底层实现不同
    主要体现在扩容机制上;
    ArrayList 前面已经说过在 JDK6- 是 x*3/2+1 ,在 JDK7+ 是 x+(x>>1)
    Vector 的扩容机制体现在 new 对象的时候,如果是 new Vector(x) 每一次就是 x*2 ;如果是 new Vector(10,x) 那么每一次就是 +x ,初始空间为 10。

  • 出现版本不同
    ArrayList 是 JDK1.2 出现的;
    Vector 是 JDK1.0 出现的,集合两大鼻祖之一。

另外,Stack 继承自 Vector 利用数组来模拟的栈结构,主要方法为 push 和 pop

Set

Collection 的另一大分支,无序、唯一的特性。

因为它是无序的,所以不再提供 get 方法来获取元素。

HashSet

平常 Set 集合中用的最多的吧,采用哈希表的结构存储数据。

然后,来说一下它的唯一:

即便是内存当中完全不同的两个对象,也有可能被视作同一个对象;

即使是内存当中完全相同的两个对象,也有可能被视为不同的对象。

这关键就看程序员怎么定义 hashCode 和 equals 方法了。

添加元素的流程

比较流程一共有三个步骤:

  1. 比较 hashCode
  2. 使用 == 比较
  3. 使用 equals 比较

使用代码来表达就是 1st && (2nd || 3rd)

首先来看哈希码是不是相同,如果不同那肯定不是一个对象;

如果哈希码相同,那么分三种情况:

  1. 内存中的同一个对象
    使用 == 检查是否是相同的对象,如果是直接就舍弃了。

  2. 程序员想要视作相同
    尊重程序员的意愿,调用 equals 进行确认

  3. 不是同一个对象,程序员也没想视作相同,是出现了哈希码冲突
    当 equals 返回 false,就是这种情况了,那么就比较这个分组的下一个/添加进集合

还有一个问题,当确认是重复元素时,采用的是先入为主的方式,后来的就被丢弃了

删除也同样尊重这些步骤,同时不再提供 remove(int) 的方式,只支持 remove(Object) 的方式删除元素,更多的是用迭代器来删除。

元素的修改

当需要修改元素时,不能直接修改,尤其是参与了 hashCode 的属性,因为修改以后 hashCode 会随之变化,但是元素在集合中的分组却不会变化,所以就会导致删删不掉,添能重复。

解决方案是在需要修改元素时,按照下面的三个步骤:

  1. 删除
  2. 修改
  3. 重新添加

这样就没什么问题了,就是操作有些繁琐。

就算修改后的 hashCode 处理后还是被分到了一组,也不可能是相同的,因为当添加时,为了避免每次都需要调用这组的 hashCode 方法比较,会把当时的 HashCode 存下来,后面就算修改了这个值也不会再变。

关于存储结构

HashSet 在 new 的时候可以传入两个参数:

  1. int 类型的分组组数
    默认为 16,最终一定是 2 的 n 次方

  2. float 类型的加载因子
    默认是 0.75F,可以是大于 1.0 的数。

至于分组组数为什么是 2 的 n 次方呢,这是因为在散列分组的时候可以把 % 替换为更高效的 & 操作,如果你传入的数是 2 的 n 次方,会自动给你设置一个接近的大于这个数的 2 的 n 次方的数。

加载因子的作用是计算阈值,阈值 = (int)(分组组数*加载因子); ,作用是控制扩容的最小临界值;也就是说,当超过这个值时才有可能进行扩容,一次扩容就是原来的分组 * 2。

扩容操作发生时,所有的老元素会重新散列,会很影响效率,所以应该尽量保证 **分组组数*加载因子>元素总量**

在这个前提下,分组组数变大,就是牺牲空间,提高效率;分组组数不变,加载因子变大,就是牺牲效率,保证节约空间

PS:HashSet 理论是可以无限延伸的,进行扩容是因为效率的原因。

// 打印距离传入的数的最小的 2 的 n 次方
public static void get(int x){
  int okay = 1;
  while(okay < x){
    okay <<= 1;
  }
  System.out.println(okay);
}

// 计算传入这个数的最小 7 的倍数
public static void get(int x){
  int okay = x/7 * 7 + 2;
}

// 传入一个三位数,抹掉个位数的零头
public static void get(int x){
  int okay = x/10 * 10;
}

补充一些相关的算法。

然后 HashSet 内部其实还是使用的是 HashMap,就是做了一层包装,大部分都是直接原封不动的调用 HashMap 的同名方法。

TreeSet

有序且唯一的单值类型集合,是 SortedSet 接口的实现。

它有一些专有方法,比如:first()、 last()、 pollFirst()、 pollLast()。

想要放入 TreeSet 集合就必须要实现 Comparable 接口,也就是实现 compareTo 方法,因为要保证有序,所以得和它说按照什么来比较。

compareTo 方法的返回值是 int,它决定着有序和唯一 ,一般来说 this 指的就是新元素,形参就是老元素。

  • 正数
    新元素更大,放在右子树(后面)

  • 负数
    新元素更小,放在左子树(前面)


  • 元素重复,舍弃

因为 TreeSet 使用的是红黑树来存储的,它是一种自平衡二叉树,每次添加元素会从根依次向下比较,最终找到自己的位置,添加元素后为保证平衡(高效)可能会进行旋转修复,也就是一天是根,不一定永世是根。

PS:优先尊重什么属性就先描述 假如什么属性不同,避免 if 的多层嵌套。

使用 TreeSet 应当尽量保证 compareTo 方法是能返回 0 的,因为它的 remove 方法依赖于 compareTo 的返回值,如果返回值永远不会返回 0(确实有这样的需求),那么就无法通过 remove 删除,只能用迭代器删。

修改元素还是要按照那三个步骤,删除、修改、重新添加;但是在迭代过程中是无法完成添加的,只能先创建一个临时的集合(不一定是 Set,可以是 LinkedList 增删快)将修改的元素加进去,等迭代完成后通过 addAll 方法放进去。

Map

然后就到键值对集合了,保存的是映射关系,主要的类有两个 HashMap 和 TreeMap。

常用的方法:put(k,v)、get(k)、containsKey(k)、containsValue(v)、remove(k)、putAll(map)

注意最后一个是 putAll 不是 addAll !!

遍历 Map 的几种方式:keySet() 、values() 、entrySet()、forEach();无论使用那种方式,得到的其实都不是一个新集合而是原本的 Map 换了个视角而已,也就是说,如果你在这些集合删除了元素,那么 map 中也会相应的删除

PS:values() 返回的是 Collection 类型的。

在 JDK8+ 中使用 forEach 更加的简单,官方 API 解释了默认实现:

for (Map.Entry<K, V> entry : map.entrySet())
     action.accept(entry.getKey(), entry.getValue());

// 使用 forEach + lambda
map.forEach((k,v) -> System.out.println(k + "--" + v));

本质上还是调用的 entrySet,但是写法上真是方便了太多太多。

Map集合添加新的键值对的时候,如果遭遇了重复的主键,那么新的主键直接舍弃,新来的值替换原来的值

HashMap

HashMap 它的 put(k,v) 、get(k) 、containsKey(k) 、remove(k),所有和主键有关的方法都尊重 hashCode/==/equals 比较机制,前面 hashSet 的时候已经说明了,毕竟 hashSet 的实现就是用的 hashMap。

包括 new 的时候也可以指定分组和加载因子。

TreeMap

它的所有主键相关的方法都尊重 compareTo 或 compare 方法,如果它们不返回 0,则:put() 永远不会舍弃元素、get() 永远直接返回 null,containsKey() 永远返回 false,remove() 永远删除失败。

HashMap和Hashtable

着重点就是他们的比较,也就是区别:

  • 它们的同步特性不同 [多线程是否安全]
    HashMap 同一时间允许多个线程同时进行操作,效率高,但是是线程不安全的。
    Hashtable 同一时间只允许一个线程进行操作,效率低,但是是线程安全的。

  • 它们对于 null 的处理不同
    HashMap 无论主键对象还是值对象都可以添加 null(主键唯一,所以主键只能放一个 null)
    Hashtable 无论主键还是值对象都不可以存放 null,否则直接 NPE

  • 它们底层实现有些许区别
    HashMap 底层默认分为 16 个小组,可以指定分组,但是最终结果一定是 2 的 n 次方数。
    Hashtable 底层默认分为 11 个小组,可以随意指定分组,最后是 % (分组组数) 实现分组。

  • 它们出现的版本不同
    HashMap 在 JDK1.2 出现;Hashtable 在 JDK1.0 出现,鼻祖之一。

再补充些关于高并发的:

JDK5.0 开始 集合的并发包当中提供了多线程高并发的场景下 更高效的 ConcurrentHashMap

并且出现了一批可以将不安全的集合转换为安全的集合的方法:

Collections.synchronizedMap(hashMap);

Collections.synchronizedList(arrayList);

Collections.synchronizedCollection();

Collections.synchronizedSet();

Collections.synchronizedSortedSet();

Collections.synchronizedSortedMap();

为什么高效?主要体现在锁的机制上,可以简单理解为 Hashtable 的锁是把整张哈希表锁了,而 ConcurrentHashMap 是锁哈希表中的某一列(每一列可以看作是一条 LinkedList),这样不同的列可以同时操作,只有同一列才会等待,既安全又高效(相比 hashtable)

关于迭代器

foreach 的实现就是用的迭代器,并且我们知道用 foreach 遍历如果进行删除(remove)操作是会抛 CME 异常的,这是因为调用 next 的时候发现 modCount 发生了变化。

那么就有一种情况是可以删除不抛异常的,那就是删除倒数第二个元素,每次循环完一次都要调用 hasNext 方法来判断是不是还有元素,这个方法的实现是这样的:

public boolean hasNext() {
  return cursor != size;
}

所以,当删除倒数第二个元素后,size 会减一,cursor 表示的是当前遍历到第几个了,那么这时 cursor 就等于了 size,认为遍历完成,所以也就不会执行 next 方法,也就不会抛出 CME(并发修改异常)。

这个了解一下就行了。

关于比较器

也就是实现了 Comparator<T> 接口的类,制定一个类的比较规则,就是如何使用比较器。

需要实现的是 public int compare(T i1,T i2) 这个方法,第一个为新元素,第二个为老元素,规则和 Comparable 一样。

使用比较器的地方常见的有两处,一个是 List 系列,使用 Collections.sort(list, com) 这个方法只适用于 List 系列;

还有一个就是 Set 系列,在 new 的时候直接传比较器就行了。

或者还可以在 lambda 里用,JDK8 的新特性。

//JDK8.0新特性 lambda表达式
Set<Integer> set = new TreeSet<>((a,b) -> b-a);
// Set<Integer> set = new TreeSet<>(new QQB());
Collections.addAll(set,55,33,11,44,22);
//我要降序
System.out.println(set);

class QQB implements Comparator<Integer>{
  @Override     //i1新来的 i2老元素
  public int compare(Integer i1,Integer i2){
    return i2 - i1;
  }
}

这就是常用的几种形式了,用在需要排序的需求上。

其他

Java 中没有函数这一叫法,统称为方法。

关于导包,JDK5 以后有一种新形势:import static java.xxx 这种相比之前加了一个 static,表示的是只导入此包的静态方法。

posted @ 2018-06-17 15:29 Kerronex 阅读(...) 评论(...) 编辑 收藏