首页 > 编程语言 > 详细

JavaSE学习总结

时间:2019-11-24 15:38:21      阅读:79      评论:0      收藏:0      [点我收藏+]

1. Java的三大特性

1.1 封装

所谓的封装即是将使用访问修饰符private将对象的属性私有化,同时提供一些可以被外界访问其属性的方法,控制在程序中属性的读写访问级别。封装的目的在于保证安全性和简化编程,调用者不需要了解具体的实现过程,只需要调用即可。

1.2 继承

所谓的继承是指可在已存在的类上进行扩展,从而获得一个新的类。新类除了拥有父类的属性和方法之外,还可以定义自己的属性和方法,实现对原有类的扩展。通过继承可以很好地复用之前的代码。

注意事项

  • 子类拥有父类对象的所有属性和方法(包括私有属性和方法),但父类的私有属性和私有方法,其子类是无法访问的,只是拥有;

  • 子类可拥有属于自己的属性和方法,实现对父类的扩展;

  • 子类可以对父类的一些方法进行自己独有的实现(即重写Override)。

1.3 多态

所谓多态,简单来说就是同一个行为(方法)在不同情况下有着不同的表现。

重点

多态有两种形式:编译时多态和运行时多态,即overload和override

  • 编译时多态,即overload:重载最常见的地方就是一个类的构造方法。一个类可以有一个或多个构造方法,这些构造方法的名字相同,但具体参数不同;

  • 运行时多态,即override:重写最常见的就是重写toString()方法。toString()是Object中的一个方法,而Object类是所有类的父类。所以当定义一个新的类时,子类可以通过重写自己的toString()方法来实现自己的特性。

多态实现的三个必要条件

  • 继承:多态中必须存在有继承关系的子类和父类;

  • 重写:子类中定义了父类中的某些具有相同签名的方法;

  • 父类引用指向子类实例。

overload和override

在分清这两个概念之前,先了解了一下方法的签名。

方法的签名(Signature)即是一个方法的组成结构,其中包括方法名,参数个数,参数类型以及参数出现的顺序,但不包括方法的返回值类型,访问修饰符及其他修饰符等。这也是为什么不能以返回值类型或者修饰符来判断一个方法是否是另一个方法的重载。

overload: 同一个类里定义了一个以上具有相同名称,但签名不同的方法,最常见的就是构造方法,有无参构造和有参构造。

override: 是指在继承的情况下,子类中定义了与父类中具有相同签名的方法,这称为子类对父类方法的重写。这是实现多态的必要步骤。

2. 关键字

2.1 访问修饰符(Modifier)

Java中的访问修饰符共有四种,其作用范围从大到小分别是:public>protected>default>private。其作用在于限制其他类对于本类中属性或者方法的访问甚至是修改。

 

作用范围当前类本包子类其他包
public
protected ×
default × ×
private   × ×

 

2.2 static

static关键字的作用主要在于方便在没有创建对象的情况下进行调用!

2.2.1 修饰成员变量

static修饰的成员变量称为静态变量,其与非静态变量的区别是,静态变量属于类所有,也就是说其可以被所有对象共享。静态变量在内存只有一个副本,仅在类被初始化加载时被初始化。非静态对象是2对象所有的,只有在创建对象的时候才会被初始化,其在内存中存在多个副本,每个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的先后进行初始化。static不能用来修饰局部变量!!!Java语法规定的

示例如下所示:

public class TestForStatic {

  public static int i;

  public int j;

  public void add(){
      i++;
      j++;
      System.out.println("i:"+i+",j:"+j);
  }


  public static void main(String[] args) {
      TestForStatic testForStatic=new TestForStatic();
      testForStatic.add();
      TestForStatic testForStatic1=new TestForStatic();
      testForStatic1.add();
  }
}

2.2 修饰代码块

代码块是Java程序中最常见的程序段。普通代码块用的最多,它就是在方法名后{...}里面的代码段。普通代码块不能单独存在,必须紧跟在方法名后面。而构造代码块是指在类中只用{...}括起来的一段代码。静态代码块即是用static修饰的构造代码块,static{...}。

构造代码块,静态代码块以及构造函数的优先级

构造函数: 只有在new一个对象时,构造函数才会被调用。同理,不new对象时,构造函数不会运行的;其作用主要是初始化对象。基本数据类型会被赋值为0;引用数据类型会被赋值为null;

构造代码块: 其作用是对对象进行初始化,即赋值;new一个对象时,构造代码块才会运行,且其优先级高于构造函数。需要注意的是类不能调用构造代码块;其与构造函数的区别在于,构造函数可以有多个,调用哪个构造函数便会建立对应的对象,因而构造函数只给对应的对象进行初始化。而构造函数是给所有对象进行统一初始化。

静态代码块: 静态代码块随着类的加载而被执行且只执行一次,并优于构造代码块和构造函数。其作用是给类初始化的。一个类可以有多个静态代码块,执行顺序按照定义的顺序来。

示例:

package clone;

/**
* @author : 11103874 Reece Lin
* @date : 2019/8/9 15:58
* @description
*/
public class Test {


  {
      System.out.println("构造代码块");
  }

  static {
      System.out.println("静态代码块");
  }

  public Test(){
      System.out.println("构造函数");
  }

  public static void main(String[] args) {
      Test test=new Test();
  }
}

控制台输出:

静态代码块
构造代码块
构造函数

在一个类里,静态变量,静态代码块,普通成员变量,构造代码块和构造函数的初始化顺序如下:

静态变量>静态代码块>普通成员变量>构造代码块>构造函数

示例:

package clone;

/**
* @author : 11103874 Reece Lin
* @date : 2019/8/9 15:58
* @description
*/
public class Test {

  private static String static_field="静态成员变量";
  private String ordinary_field="普通成员变量";

  {
      System.out.println(ordinary_field+" "+1);
      System.out.println(static_field+" "+1);
      System.out.println("构造代码块");
  }

  static {
      System.out.println(static_field+" "+2);
    //   System.out.println(ordinary_field); Error
      System.out.println("静态代码块");
  }

  public Test(){
      System.out.println("构造函数");
  }

  public static void main(String[] args) {
      Test test=new Test();
  }
}

控制台输出:

静态成员变量 2
静态代码块
普通成员变量 1
静态成员变量 1
构造代码块
构造函数

static的作用范围

由上可以看出,静态成员变量可以出现在构造代码块中,而普通成员变量却不能出现在静态代码块中。这是因为,由static修饰的成员变量或者方法是属于类的。JVM在加载这个类的时候,会首先初始化static修饰的成员变量或者方法,他们的引用不借助于对象的实例化,即new出一个对象来。而普通成员变量或者方法均是实例变量或者方法,他们的引用依赖于实例的生成.

静态成员变量或者方法可直接通过类名.变量名/方法名来调用,也可以通过对象.变量名/方法名来调用.

2.3 final

Java中可以使用final来修饰类,方法和变量(成员变量和局部变量)

2.3.1 修饰类

当使用final修饰一个类时,表明这个类不能被继承.最常见的就是String类.还有,当使用final修饰类时,类中所有成员方法都会被隐式地指定为final方法.

2.3.2 修饰变量

当final修饰变量的时候,这个变量就变成了常量,且必须在定义或者构造代码块/构造函数中进行初始化赋值.

  • 当final修饰基本数据类型时,其数值在初始化之后就不能再更改了;

  • 当final修饰引用类型时,则初始化之后变量不能再指向另一个对象,但对象的内容可以改变.

 public static void main(String[] args) {
    final StringBuffer stringBuffer=new StringBuffer("final");
      System.out.println(stringBuffer.toString());
      // stringBuffer=new StringBuffer("final test"); 报错
      stringBuffer.append(" test");
      System.out.println(stringBuffer.toString());
  }

控制台输出:

final
final test

2.3.3 修饰方法

当final修饰方法时,此方法不可被重写.

2.4 this,super

2.4.1 this

this关键字在Java中作为自身的引用,其作用就是在类中的方法中引用该类自身。在一个类中,成员变量是不能重名的,但方法或者代码块中的局部变量却可以和成员变量重名,此时就要使用this关键字来进行区分。最常见的就是构造方法里this的使用。方法间的相互调用也可以使用this关键字。

public class Person {
  private String name;
  private int age;
   
  public Person(String name, int age) {
      this.name = name;
      this.age = age;
  }
}

2.4.2 super

super在Java中是对父类对象的引用。若子类中定义了与父类重名的属性或方法时,有可能发生子类属性或方法覆盖父类属性或方法的情况。这种情况下,若想调用父类的属性或方法,可以用super关键字调用父类的属性或方法。最常见的就是子类的构造方法中对父类构造方法的调用。

public class Person {
  private String name;
  private int age;

  public Person() {
  }

  public Person(String name, int age) {
      this.name = name;
      this.age = age;
  }

  class User extends Person{

      public User() {
          super();
      }

      public User(String name, int age) {
          super(name, age);
      }
  }
}

3 抽象类和接口

3.1 抽象类

使用abstract关键字修饰的类称之为抽象类。抽象类是对类的抽象,如果一个类没有包含足够多的信息来描述一个具体的对象,这样的类就可以称之为抽象类。

抽象类的特点总结如下:

  • 抽象类不能被实例化,若子类继承了抽象类,且子类不是抽象类,则可以定义父类引用指向子类实例;

  • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类;

  • 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能;

  • 构造方法, static 修饰的类方法不能声明为抽象方法;

  • 若子类继承了抽象类,则必须对父类中的抽象方法进行重写,否则该子类也要声明为抽象类;

  • 可含有普通成员变量,访问修饰符任意;

  • 可含有静态成员变量,访问修饰符任意;

  • abstract不能与final同时修饰同一类。因为final修饰的类是不可以被继承的,抽象类被继承才有意义;

  • abstract不能与private,static,final或者native同时用一个方法。

3.2 接口

接口主要用来描述一个类具有什么样的功能,但并不给出具体的实现。因而接口是实际上对类行为的一种抽象,是抽象方法的集合。同抽象类一样,接口也不能直接通过new来实例化一个对象,但可以实例化它的实现类。若一个类实现一个接口,则该类必须全部实现接口的所有抽象方法;与抽象类不同的是,接口中的抽象方法并不需要使用abstract关键字。

  • 接口并不是类,所以其内部不能存在构造方法;

  • 接口中所有的方法均会被自动地声明为public,若使用protected,private会导致编译错误;

  • 接口中可以定义成员变量,但会被自动地声明为public static final,即常量,且必须被显示初始化;

  • 接口中除了使用static修饰的方法必须有实现体之外,其余均为抽象方法,且static与final不能连用

  • 若一个类实现一个接口,则必须实现接口的中所有方法,但抽象类不需要

接口和抽象类的区别

 

参数接口抽象类
关键字 子类使用implements关键字来实现接口,子类必须实现接口中所有的方法; 子类使用extends关键字来继承抽象类,若子类不实现父类中的抽象方法,则子类必须声明为抽象类;若子类实现父类中的全部抽象方法,则子类则普通类。
变量 可以有普通成员变量,但默认修饰为public static final 可以有普通成员变量,访问修饰符任意
方法 除了使用static修饰的方法有实现体,其余均为抽象方法 可以有抽象方法,也可以有普通成员方法或普通静态方法
构造器 接口并不是类,因而其没有构造器 抽象类中可以有构造器
main 没有main方法 可以有main方法并运行
多继承 接口可以继承另一个接口的同时实现多个其他接口 抽象类可以继承一个类的同时实现多个接口
增加新方法 若在接口中增加新方法,则所有实现该接口的类均必须实现该新方法 若在抽象类中增加一个抽象方法,则所有继承了该抽象类的子类必须重写该抽象方法;若在抽象类中定义一个普通方法,则子类可重写,也可不重写
速度 比接口速度要快 较慢,因为需要寻找要实现的方法
设计理念 like-a,一种功能扩展的关系 is-a的关系,体现一种关系的延续

 

3.3 内部类

3.3.1 普通内部类:嵌套在一个类里面,

  • 访问修饰符:public,protected,default,private均可;

  • 可以使用final修饰属性或者方法,不能使用static单独修饰属性或方法,要与final连用;

  • 内部类可以访问外部类的所有属性或方法,包括私有;

3.3.2 局部内部类:嵌套在一个方法里面

  • 只能用default修饰类

  • 可以用final修饰属性或方法,不能使用static单独修饰或方法,要与final连用

  • 可以访问外部类的所有属性或方法,包括私有;

3.3.3 匿名内部类:没有名字,通常用来实现接口

  • 没有类名,没有修饰符

  • 可以用final修饰属性或方法,不能使用static单独修饰或方法,要与final连用

  • 可以访问外部类的所有属性或方法,包括私有;

3.3.4 静态内部类:使用static修饰的内部类

  • 静态内部类可以有静态域和方法

  • 接口中的内部类自动修饰为public static

  • 作用就是将一个类隐藏在另一个类的内部,且使得内部类无法引用外部类的对象

3.4 反射

3.4.1 反射机制

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

反射机制的作用

  • 可在运行阶段判断一个对象所属的类;

  • 可以在运行阶段构造一个类的对象;

  • 可以在运行阶段获得一个类所具有的构造器,属性和方法;

  • 可以在运行阶段调用一个对象的方法,或者设置其成员变量属性值。

3.4.2 反射API

3.4.2.1 Class类

Class类:当Java虚拟机载入一个类时,JVM会自动为为这个类创建一个Class对象,这个Class对象保存着这个类的相关信息。

获得Class对象的方法

  • Class clazz=Class.forName(String className);

  • Class clazz =类名.class;

  • Class clazz= 实例对象.getClass();

3.4.2.2 Constructor类

Constructor类表示类的构造方法。常用API如下:

 

方法名称方法返回值方法说明
getConstructor() Constructor<?>[] 获得一个所有访问权限为public的构造函数对象数组
getDeclaredConstructor() Constructor<?>[] 获得一个包含任意访问修饰符的构造函数对象数组
getConstructor(Class<?>... parameterTypes) Constructor<T> 获得一个具有指定参数且访问权限为public的构造函数对象
getDeclaredConstructor(Class<?>... parameterTypes) Constructor<T> 获得一个任意访问权限的具有指定参数的构造函数对象
newInstance() T 创建一个新实例对象

 

3.4.3 Field类

Filed类表示类的属性。常用API如下:

 

方法名称方法返回值方法说明
getFiled() Filed[] 获得一个访问权限为public的属性对象数组
getField(String name) Filed 获得一个指定名称且访问权限为public的属性对象
getDeclaredFiled() Filed[] 获得一个具有任意访问权限的属性数组对象
getDeclaredFiled(String name) Filed 获得一个指定名称且访问权限任意的属性对象
setAccessible(boolean true) void 将private修饰的属性设置为可访问
set(Object object,Object value) void 将指定对象变量上此 Field 对象表示的字段设置为指定的新值
get(Object object) Object 获得属性的赋值
getName() String 获得属性的名称

 

3.4.4 Method类

Method类表示类的方法。常用API如下:

 

方法名称方法返回值方法说明
getMethod() Method[] 获得一个访问权限为public的方法对象数组
getMethod(String name, Class<?>... parameterTypes) Method 获得一个指定名称和指定参数且访问权限为public的方法对象
getDeclaredMethod() Method[] 获得一个任意访问权限的方法对象数组
getDeclaredMethod(获得一个指定名称和指定参数且访问权限为public的方法对象) Method 获得一个指定名称和指定参数且访问权限任意的方法对象
getName(0) String 获得方法的名称
setAccessible(boolean true) void 将private修饰的方法设置为可访问
invoke(Object object,Object... args) Object 对带有指定参数的指定对象调用该方法

 

Java学习总结2

异常

异常是指程序中的一些错误。异常机制的作用在于,当程序出现错误使得某些操作没有完成时,程序应该返回一种安全状态,并能够让用户执行一些其他的命令;或者是允许用户保存所有操作的结果,并以妥善的方式终止程序。

在Java中,所有异常对象都是派生于Throwable类的一个实例。

1.1 异常种类

异常类分为两大类:错误Error和异常Exception,它们的父类均是Throwable类。

1.1.1 Error

Java中Error是指表示不希望被程序捕获或者是程序无法处理的错误。 Error类对象通常是由JVM生成并抛出,这些错误一般情况下是由于Java运行时系统的内部错误或者是资源耗尽错误,如常见的OutOfMemeory,StackOverFlowError,NoClassDefError等。这些错误通常是不可查的,因为他们在程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。当出现这些错误时,JVM一般会终止线程。

1.1.2 Exception

Java中Exception是指表示程序可能捕捉到的异常或者是程序可以进行处理的异常。其中Exception可分为运行时异常(RuntimeException)和非运行时异常。

1.1.2.1 RuntimeException

RuntimeException,顾名思义,即程序在运行时出现的异常。这类异常的特点是在编译时,编译器并不会强制程序员进行异常处理,只有在运行时出现异常时,才会交由JVM进行处理。常见的RuntimeException有ArrayIndexOutOfBoundsException,ArithmeticException, ClassCastException,NullPointerException等等。

1.1.2.2 非运行时异常

除了RuntimeException之外统称为非运行时异常。这些异常从Java语法上来将是必须进行处理的异常,如果不进行处理,则程序不能通过编译。常见的如IOException,SQLException以及继承了Exception的自定义Exception类。

1.1.3 UncheckedException和CheckedException

1.1.3.1 UncheckedException

UncheckedException,即非受查异常,这类异常编译器一般不强制要求进行处理,但也可进行捕获处理。这些异常一般是由程序逻辑错误引起的,因此在编写程序时尽量从逻辑角度避免这类异常的发生。RuntimeException及其子类和Error均是非受查异常。

1.1.3.2 CheckedException

CheckedException,即受查异常,这类异常编译要求必须进行处理,否则编译不通过。除了RuntimeException及其子类之外,其他的Exception,包括自定义的Exception类均是受查异常。当程序中出现这类异常,要么抛出异常,要么捕捉异常。

1.2 异常处理方式

如上所诉,当程序中出现受查异常时,要么捕捉异常,要么抛出异常。

1.2.1 声明异常(throws)

若一个方法中存在可能导致异常出现的代码,则可以在声明的方法名后加一个throws子句来声明可能出现的异常类型,从而使调用者来处理异常。throws后面应列出所有可能出现的异常,不同异常类型之间使用逗号","分割,否则无法编译通过。

   public void test() throws FileNotFoundException,IOException,Exception {
      InputStream in=new FileInputStream("");
      //...
  }

1.2.2 捕捉异常

1.2.2.1 try-catch

当程序中出现异常时,可使用try-catch语句块来进行异常处理。

实例:

 public int test(int a,int b) {
      int result= 0;
      try {
          result = a/b;
      } catch (ArithmeticException e) {
          e.printStackTrace();
      }
      return result;
  }

上述代码中,try{}将可能出现异常的代码块包起来,若运行过程中程序出现异常,则创建异常对象,这个异常对象会与catch子句中的对象进行匹配,若匹配成功则会执行catch子句中的异常处理代码。

1.2.2.2 多重catch语句

某些情况下,一段代码中可能会导致多种异常出现,这个时候可以使用try-catch多重语句块进行处理。处理这种情况,我们需要定义两个或者更多的catch子句,每个子句捕获一种类型的异常,当异常被引发时,每个catch子句被依次检查,当某个catch子句中的异常类型符合时,将会执行该catch子句中的异常处理代码,剩下的catch子句将被短路,不会再被执行。

实例:

 public void add() {
      Scanner sc=new Scanner(System.in);
      System.out.println("程序开始");
    try{
        System.out.println("请输入第一个数");
        int first=sc.nextInt();
        System.out.println("请输入第二个数");
        int second=sc.nextInt();
        int result=first/second;
        System.out.println("结果是:"+result);
    }catch (InputMismatchException e){
        e.printStackTrace();
    }catch (ArithmeticException e){
        e.printStackTrace();
    }catch (Exception e){
        e.fillInStackTrace();
    }

      System.out.println("程序结束");
  }

多重catch语句块中要注意的是,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。

1.2.2.3 try-catch-finally

某些情况下,代码中会设计到某些系统资源的使用,若是代码中出现异常,将会使得剩下的代码停止执行,从而使用系统资源无法关闭。这种情况下,可以在try-catch语句块后加一个finally语句块。对于try{}中的语句块中而言,无论代码是否会导致异常的发生,finally语句块中代码一定会被执行 。

示例:

public void IoTest() throws IOException{
      InputStream in=null;
      try {
          in=new FileInputStream("fileName");
          //...
      } catch (FileNotFoundException e) {
          e.printStackTrace();
      }finally {
          in.close();
      }
  }

1.2.2.4 try-with-resource

上述涉及到资源关闭的代码可以使用try-with-resource结构来改写。

示例:

 public void IoTest() throws IOException{
      try(InputStream in=new FileInputStream("")) {
          //....
      }

无论接下来的代码中是否出现异常,都会调用in.close()方法来关闭资源。

1.2.2.5 try-catch-finally中return关键字的使用

若finally语句块中没有return语句,则会返回try或者catch中的return结果,且finally中的语句块会在return之前执行完;若finally里含有return语句,则无论try或者catch中是否含有return语句,都只会返回finally中的return结果。

1.2.3 抛出异常(throw)

在已经声明异常的方法或者try-catch语句块中,若出现了异常,还可通过throw关键字将异常对象抛出,由程序的调用者进行处理。

示例:

     public void add() {
      Scanner sc = new Scanner(System.in);
      System.out.println("程序开始");
      try {
          System.out.println("请输入第一个数");
          int first = sc.nextInt();
          System.out.println("请输入第二个数");
          int second = sc.nextInt();
          int result = first / second;
          System.out.println("结果是:" + result);
      } catch (InputMismatchException e) {
          e.printStackTrace();
          throw new InputMismatchException("请输入数字");
      } catch (ArithmeticException e) {
          e.printStackTrace();
          throw new ArithmeticException("除数不能为0");
      } catch (Exception e) {
          e.fillInStackTrace();
      }
      System.out.println("程序结束");
  }

泛型

泛型: 即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,如常见的ArrayList和HashMap。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法

1.引入泛型的意义

在Java引入泛型之前,ArrayList就已经存在了。但ArrayList类只维护了一个Object引用的数组。

public class ArrayList{
private Object[] elementData;
...
public Object get(int i){}
public void add(Object x){}
}

这就带来了两个问题,一是当获取其中的一个值时必须先进行强制类型转换,将其转换为需求的对象类,如:

ArrayList files=new ArrayList();
file.add("fileName");
String fileName=(String)files.get(0);

二是没有错误检查,我们可以向Object数组添加任何类的对象,如:

File file=new File("...");
file.add(file);

对于这个add()方法,编译和运行的都不会报错,然而若是讲取出来的元素强制转换为其他类型,就会产生错误。如:

//String fileName2=(String)files.get(1); 运行时会抛出错误
java.lang.ClassCastException: java.io.File cannot be cast to java.lang.String

因而为了解决上述两个问题,Java1.5中引入泛型。泛型提供了一个类型参数(type parameter),用来指示容器内部元素的类型,从而在编译阶段就可以避免出现错误。

引入泛型的好处:

类型安全

  • 提高Java程序的类型安全;

  • 编译的时候就可以检查出是否会出现ClassCastException;

  • 符合越早出错代价越小原则;

消除强制类型转换

  • 使用时可以直接得到目标类型,可读性增强,同时消除了强制类型转换;

2. 泛型的使用

2.1 泛型类

如果想要类具备很好的扩展性,那么就可将其设置为泛型类。它与普通类的区别在于声明的时候类名后加一个类型参数列表<E>,类型参数可以为一个或多个,如<String><String,Integer>等。泛型类最常见的用途就是作为容纳不同类型数据的容器类,集合容器就是这样。

示例:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
  //key这个成员变量的类型为T,T的类型由外部指定  
  private T key;

  public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
      this.key = key;
  }

  public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
      return key;
  }
}

需要注意的是,T必须是引用数据类型,不能是基本数据类型。

2.2 泛型方法

泛型方法既可以存在于泛型类中,也可存在于普通类中。如果泛型方法可以解决问题,那就应该尽量使用泛型方法。泛型方法的定义如下:

  • 泛型方法声明都有一个类型参数声明部分(尖括号中的参数),该类型参数声明部分在方法返回类型之前;

  • 每个类型参数声明部分可包含一个或多个类型参数,逗号分隔开。一个泛型参数,也被称为一个类型变量,用于指定一个泛型类型名称的标识符;

  • 参数类型只能是引用数据类型,不能使用基本数据类型;

  • 类型参数可用来声明返回值类型,如私有属性的get()方法,并且能作为泛型方法得到的实际参数类型的占位符。

示例:

//泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class GenericMethodTest <T> {
  //data这个成员变量的类型为T,T的类型由外部指定  
  private T data;
    //这不是泛型方法,只是泛型类的普通成员方法
  public T getData() {
      return data;
  }

  public void setData(T data) {
      this.data = data;
  }
   
  public GenericMethodTest(){
   
  }
//泛型构造方法形参data的类型也为T,T的类型由外部指定
  public GenericMethodTest(T data){
        this.data=data;
  }

  //这是一个泛型方法 访问修饰符与返回值之间的<T>必不可少,这表明是一个泛型方法,并且声明了一个泛型T。这个T可以出现在这个泛型方法的任意位置,泛型的数量可以为任意个.方法名getValue()前面的T是返回值类型。
  public <T> T getValue(T t){
      return t;
  }

  public static void main(String[] args) {
      GenericMethodTest<String> gmt=new GenericMethodTest();

      System.out.println(gmt.getValue("name"));
  }
}

控制台输出:

name
5

2.3 泛型接口

如同泛型类一样,在接口后面增加一个类型参数即为泛型接口,实现的时候要指定参数类型,否则默认Object。

public interface GenericInterface <T> {

  /**
    * 打印数组
    * @param ts
    */
    void printArray(T[] ts);
  /**
    * 返回数组中的值
    * @param ts
    * @return
    */
  T getValues(T[] ts);
}
public class GenericInterfaceTest implements GenericInterface<String>{

  @Override
  public void printArray(String[] strings) {
      for (String s :strings){
          System.out.printf("%s ",s);
      }
      System.out.println();
  }

  @Override
  public String getValues(String[] strings) {
      StringBuffer buffer=new StringBuffer("");
      for (String s:strings){
          buffer.append(s+" ");
      }
      return buffer.toString();
  }

  public static void main(String[] args) {
      String[] strs=new String[]{"Jack","Mike","Jane","Maria"};
      GenericInterfaceTest test=new GenericInterfaceTest();
      test.printArray(strs);
      System.out.println("========");
      System.out.println(test.getValues(strs));

  }
}

2.4 通配符

有时候希望可以限制传入的参数类型,从而进行一些特定的操作,这个时候就可以使用通配符了。

通配符有三种形式:

  • <?> 无限制通配符;

  • <? extends T> extends关键字声明了类型的上界,表示参数化的类型可能是所指定的类型或者其子类;

  • <? super T> super关键字声明了类型的下界,表示参数化的类型可能是指定的类型或其父类;

  • <?>和<? extends E>用于实现更为灵活的读取,它们可以用类型参数的形式替代,但通配符形式更为简洁;

  • <? super E>用于实现更为灵活的写入和比较,不能被类型参数形式替代

2.4.1 <?>

类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List<String>,List<Integer> 等所有List<具体类型实参>的父类。

public class WildCardTest {
   
  public static void getFirstValue(List<?> list){
      System.out.println("FirstValue: "+list.get(0));
  }
   
  public static void main(String[] args) {
      List<Integer> list1=new ArrayList<>();
      list1.add(1);
      list1.add(2);
      List<String> list2=new ArrayList<>();
      list2.add("name1");
      list2.add("name2");

      WildCardTest.getFirstValue(list1);
      WildCardTest.getFirstValue(list2);
  }
}

控制台:

FirstValue: 1
FirstValue: name1
FirstValue: 15

2.4.2 <? extends T>

<? extends T>如此定义就是通配符泛型值接受T及其子类。

public class WildCardTest {

  public static void getUpperNumber(List<? extends Number> list){
      System.out.println("Value: "+list.get(0));
  }

  public static void main(String[] args) {
      List<Integer> list1=new ArrayList<>();
      list1.add(1);

      List<String> list2=new ArrayList<>();
      list2.add("name1");

      List<Number> list3=new ArrayList<>();
      list3.add(15);

      WildCardTest.getUpperNumber(list1);
    // WildCardTest.getUpperNumber(list2); 报错 需要输入Number类而不是String类
      WildCardTest.getUpperNumber(list3);
  }
}

2.4.3 <? super T>

<? super T>如此定义就是通配符泛型值接受T及其父类

public class WildCardTest {

  public static void getLowerInteger(List<? super Integer> list){
      System.out.println("value: "+list.get(0));
  }
   
  public static void main(String[] args) {
      List<Integer> list1=new ArrayList<>();
      list1.add(1);

      List<String> list2=new ArrayList<>();
      list2.add("name1");

      List<Number> list3=new ArrayList<>();
      list3.add(15);

      WildCardTest.getLowerInteger(list1);
  //     WildCardTest.getLowerInteger(list2); 报错 需要输入Number类而不是String类
      WildCardTest.getLowerInteger(list3);
  }
}

2.5 泛型擦除

在Java中,泛型是Java编译器的概念,用泛型编写的Java程序和普通的Java程序基本相同,只是多了一些参数化的类型同时少了一些类型转换,这是因为JVM中并没有泛型类型对象,所有的对象都是普通类,JVM运行时对泛型几乎一无所知。

当编译器对带有泛型的Java代码进行编译时,编译器会执行类型检查和类型推断,然后生成不带泛型的普通字节码文件,这种普通的字节码被JVM接收并执行,这种操作叫做类型擦除

类型擦除示例:

public class GenericErasureTest {

  public static void main(String[] args) {
      List<String> strs=new ArrayList<>();
      List<Integer> ins=new ArrayList<>();
       
System.out.println(strs.getClass()==ins.getClass());
  }
}

控制台输出:true

出现这种的原因就是泛型的擦除。事实上,无论是String还是Integer,集合框架中存放的数据类型始终是Object类型,这个从集合框架的源码和反射可以看出。

2.5.1 擦除实现原理

Java 编辑器会将泛型代码中的类型完全擦除,使其变成原始类型。当然,这时的代码类型和我们想要的还有距离,接着 Java 编译器会在这些代码中加入类型转换,将原始类型转换成想要的类型。这些操作都是编译器后台进行,可以保证类型安全。总之泛型就是一个语法糖,它运行时没有存储任何类型信息。

2.5.2 擦除导致的泛型不可变性

泛型中没有逻辑上的父子关系,如List并不是List的父类。两者擦除之后都是List,如下所示,编译器会报错。

public interface ErasureTest {
  //void add(List<String> strs); 报错 add(List<String> clashes with List<Integer>; both methods have same erase)
  void add(List<Integer> ins);
}

泛型的这种情况称为 不可变性,与之对应的概念是 协变、逆变:

协变:如果 A 是 B 的父类,并且 A 的容器(比如 List< A>) 也是 B 的容器(List< B>)的父类,则称之为协变的(父子关系保持一致) 逆变:如果 A 是 B 的父类,但是 A 的容器 是 B 的容器的子类,则称之为逆变(放入容器就篡位了) 不可变:不论 A B 有什么关系,A 的容器和 B 的容器都没有父子关系,称之为不可变 Java 中数组是协变的,泛型是不可变的。

如果想要让某个泛型类具有协变性,就需要用到边界。

2.5.3 擦除拯救者:边界

泛型程序编译时会被泛型擦除从而变成原始类型,这就使得许多操作无法进行,如果没有指定边界,类型参数将被擦除为Object。若想让参数保留一个边界,可以给参数设置一个边界,泛型参数将会被擦除到它的第一个边界(边界可以有很多个),这样即使运行时擦除后也会有范围。

示例:

public class EraseBoundTest {
  interface Game {
      void play();
  }
  interface Program{
      void code();
  }

  public static class People<T extends Program & Game>{
      private T mPeople;

      public People(T people){
          mPeople = people;
      }

      public void habit(){
          mPeople.code();
          mPeople.play();
      }
  }

  public static void main(String[] args) {
      EraseBoundTest test=new EraseBoundTest();
      System.out.println(test);
  }
}

利用IDEA查看其class文件,如下:

public class EraseBoundTest {
  public EraseBoundTest() {
  }

  public static void main(String[] args) {
      EraseBoundTest test = new EraseBoundTest();
      System.out.println(test);
  }

  public static class People<T extends EraseBoundTest.Program & EraseBoundTest.Game> {
      private T mPeople;

      public People(T people) {
          this.mPeople = people;
      }

      public void habit() {
          this.mPeople.code();
          //编译器将类型参数替换为第一个边界的类型
          ((EraseBoundTest.Game)this.mPeople).play();
      }
  }

  interface Program {
      void code();
  }

  interface Game {
      void play();
  }
}

参数 T 有两个边界,编译器事实上会把类型参数替换为它的第一个边界的类型,这就是边界的作用。

2.6 泛型使用规则

2.6.1 泛型的规则

  • 泛型的参数类型只能是类(包括自定义类),不能是简单类型。

  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

  • 泛型的类型参数可以有多个

  • 泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”

  • 泛型的参数类型还可以是通配符类型,例如 Class

2.6.2 泛型的使用场景

当类中要操作的引用数据类型不确定的时候,过去使用 Object 来完成扩展,JDK 1.5后推荐使用泛型来完成扩展,同时保证安全性。

2.7 总结

2.7.1 移植兼容性

上面说到使用 Object 来达到复用,会失去泛型在安全性和直观表达性上的优势,那为什么 ArrayList 等源码中的还能看到使用 Object 作为类型?

根据《Effective Java》中所述,这里涉及到一个 “移植兼容性”:

泛型出现时,Java 平台即将进入它的第二个十年,在此之前已经存在了大量没有使用泛型的 Java 代码。人们认为让这些代码全部保持合法,并且能够与使用泛型的新代码互用,非常重要。

这样都是为了兼容,新代码里要使用泛型而不是原始类型。

2.7.2 擦除

泛型是通过擦除来实现的。因此泛型只在编译时强化它的类型信息,而在运行时丢弃(或者擦除)它的元素类型信息。擦除使得使用泛型的代码可以和没有使用泛型的代码随意互用。

2.7.3 通配符的使用

如果类型参数在方法声明中只出现一次,可以用通配符代替它。

比如下面的 swap 方法,用于交换指定 List 中的两个位置的元素:

private <E> void swap(List<E> list, int i, int j) {
  //...
}
1
2
3
只出现了一次 类型参数,没有必要声明,完全可以用通配符代替:

private void swap(List<?> list, int i, int j){
  //...
}
1
2
3

对比一下,第二种更加简单清晰吧。

2.7.4 数组中不能使用泛型

这可能是 Java 泛型面试题中最简单的一个了,当然前提是你要知道 Array 事实上并不支持泛型,这也是为什么 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。

2.7.5 Java 中 List<Object> 和原始类型 List 之间的区别?

原始类型和带参数类型 之间的主要区别是:

在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查 通过使用 Object 作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String 或 Integer。你可以把任何带参数的类型传递给原始类型 List,但却不能把 List< String> 传递给接受 List< Object> 的方法,因为泛型的不可变性,会产生编译错误。

集合

1. 常见集合种类

Collection和Map接口是所有集合框架的父接口。

  • 实现Collection接口的主要有Set接口和List接口;

  • 实现Set接口的主要有HashSet,TreeSet和LinkedHashSet等;

  • 实现List接口的主要有ArrayList,LinkedList,Stack和Vector;

  • 实现Map接口的实现类主要有:HashMap,TreeMap,HashTable和ConcurrentHashMap;

1.1 List,Set和Map的区别

  • List:允许集合存在重复的元素,且集合中元素有序;

  • Set:不允许集合中存在重复的元素,集合中元素无序;

  • Map:使用Key-Value键值对来存储。Map会维护与Key相关联的值。键Key不允许重复,否则会存储最近一次实现put操作的value;value值可以重复。

2 List

2.2 ArrayList

ArrayList是一个继承了AbstractList,实现List和RandomAcess等接口的可以动态增大容量的数组,其底层是一个Object数组,如下所示:

public class ArrayList<E> extends AbstractList<E>
      implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
}

特点:

  • ArrayList容量不固定,随着储存容量的增大而动态扩容;

  • ArrayList允许存在重复的元素,且元素有序排列;

  • 插入的元素可以null;

  • 非线程安全。

2.4 Vector

Vector与ArrayList一样,其底层也是一个Object数组,但与ArrayList不同的是,其内部的方法多是线程同步,即某一时刻只有一个线程可以读写Vector,因而可在多线程下使用。但也因此,其相对ArrayList来说较慢。

public class Vector<E>
  extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}

2.3 LinkedList

LinkedList(链表)是一个继承了AbstractSequentiaList,实现了List接口和Dqueue接口的双向链表。

public class LinkedList<E>
  extends AbstractSequentialList<E>
  implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{}

LinkedList中含有三个成员变量,分别为 int size, Node<E> first和Node<E> second。其中first表示该节点的上一个节点,second表示该节点的下一个节点,size表示当前链表中的元素个数。

 transient int size = 0;
transient Node<E> first;
transient Node<E> last;

节点是一个双向节点:

private static class Node<E> {
      E item;
      Node<E> next;
      Node<E> prev;

      Node(Node<E> prev, E element, Node<E> next) {
          this.item = element;
          this.next = next;
          this.prev = prev;
      }
  }

ArrayList,LinkedList和Vector之间的对比:

  • ArrayList底层是一个数组,而LinkedList底层是一个双向链表;

  • ArrayList可以通过索引index直接访问元素,速度较快,而LinkedList只能通过遍历一个个结点去寻找,速度较慢;

  • 当向ArrayList的指定位置上添加或删除一个元素时,指定位置前后的元素都要进行移动,效率较低;当向LinkedList的指定位置添加或删除一个元素时,由于其底层是双向链表,速度较快;

  • ArrayList的空间浪费主要体现在list列表的结尾会预留一定的容量空间,而LinkedList由于其每个元素中都存储了一个前继结点的引用和一个后继结点的引用,因而其占据空间较大;

  • ArrayList和LinkedList都是非线程安全的,效率相对较高;Vector是线程安全的,因而效率较低。

List常用API:

add(int index,E element) 将数据新增到指定位置 add(E element) 将指定的元素添加到此列表的尾部。 addAll(Collection<? extendsE> c) 按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。 addAll(int index,Collection<? extendsE> c) 从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。 clear() 移除此列表中的所有元素。 set(int index,E element) 用指定的元素替代此列表中指定位置上的元素。 get(int index) 返回此列表中指定位置上的元素。 size() 查看元素总个数 contains() 包含

3.Map

Map,是一种存储键值对映射的容器类。Map中键(key)可以为任意类型的对象,不可重复,也不可为null;值可以为任意类型的对象,可重复,可为null。

3.1 HashMap

HashMap是基于hash table的Map接口的非同步实现。它继承自AbstractMap类,实现了Map,Cloneable,Serializable接口。

public class HashMap<K,V> extends AbstractMap<K,V>
  implements Map<K,V>, Cloneable, Serializable {}

特点:

  • key-value对形式,根据一个对象去查找另一个对象;

  • 当存储的元素超过一定范围时,可动态扩容;

  • 可存储null值;

  • 非线程安全。

    HashMap的底层实现:

    JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。如下图所示。

 

HashMap 通过 key 的 hashCode 经过hash()方法处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

hashCode()方法

public int hashCode() {
      int h = 0;
      Iterator<Entry<K,V>> i = entrySet().iterator();
      while (i.hasNext())
          h += i.next().hashCode();
      return h;
  }

hash()方法

   static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

但是这种结构也有很大的弊端。若储存的元素过多,链表的长度会逐渐增加,需要一个个遍历过去,将会话费O(n)的查找时间。因而在JDK1.8中,HashMap采用数组+链表+红黑树来实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。其数据结构如下所示:

 

HashMap存储元素的数组:

数组的元素类型是Node<K,V>,Node<K,V>继承自Map.Entry<K,V>,表示键值对映射。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        //构造函数 ( Hash值键值下一个节点 )
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

HashMap的put()方法:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; 
    Node<K,V> p;
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length; //如果没有初始化则初始化table
    if ((p = tab[i = (n - 1) & hash]) == null)
        //根据hash值得到这个元素在数组中的位置 如果此位置还没别的元素 直接存放
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //应该放在首节点的位置
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //红黑树插入新节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //链表插入新节点
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //超过阈值 将链表转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //更新hash值和key值均相同的节点Value值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

HashMap的get()方法:

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; 
    Node<K,V> first, e; 
    int n; K k;
    //如果当前Map里还没元素或者hash值对应的链表头节点为空 直接返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //是头节点 直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //非头节点 遍历找到对应key的值
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

Map常用API:

clear() 从 Map 中删除所有映射 remove(Object key) 从 Map 中删除键和关联的值 put(Object key, Object value) 将指定值与指定键相关联 putAll(Map t) 将指定 Map 中的所有映射复制到此 map entrySet() 返回 Map 中所包含映射的 Set 视图。Set 中的每个元素都是一个 Map.Entry 对象,可以使用 getKey() 和 getValue() 方法(还有一个 setValue() 方法)访问后者的键元素和值元素 keySet() 返回 Map 中所包含键的 Set 视图。删除 Set 中的元素还将删除 Map 中相应的映射(键和值) values() 返回 map 中所包含值的 Collection 视图。删除 Collection 中的元素还将删除 Map 中相应的映射(键和值) get(Object key) 返回与指定键关联的值 containsKey(Object key) 如果 Map 包含指定键的映射,则返回 true containsValue(Object value) 如果此 Map 将一个或多个键映射到指定值,则返回 true isEmpty() 如果 Map 不包含键-值映射,则返回 true size() 返回 Map 中的键-值映射的数目

3. Set

3.1 HashSet

HashSet继承了AbstractSet,实现了Set,Cloneable和Serializable接口。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{}

特点:

  • 集合中的元素不能重复;

  • 集合中的元素是无序的;

  • 可以存储null;

  • 底层是基于HashMap实现的;

  • 非线程安全;

HashSet如何检查重复: 当执行add()方法时,HashSet会先计算对象的hashCode()值来判断对象加入的位置,同时会与其他的元素的hashCode值进行比较。若无相符的hashCode,即集合中不存在这个值,直接存入;若存在相同的hashCode,则拒绝将这个元素加入集合。

Set常用接口:

boolean add(E e) 如果此 set 中尚未包含指定元素,则添加指定元素 void clear() 从此 set 中移除所有元素 Object clone() 返回此 HashSet 实例的浅表副本:并没有复制这些元素本身 boolean contains(Object o) 如果此 set 包含指定元素,则返回 true boolean isEmpty() 如果此 set 不包含任何元素,则返回 true Iterator< E > iterator() 返回对此 set 中元素进行迭代的迭代器 boolean remove(Object o) 如果指定元素存在于此 set 中,则将其移除 int size() 返回此 set 中的元素的数量(set 的容量)

JavaSE学习总结

原文:https://www.cnblogs.com/reecelin/p/11922269.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!