Java异常处理+常用类+泛型

目录

异常处理

捕获

抛出

自定义异常类

 finally

String

创建方法

比较

拼接

常用方法

length

equals

starsWith和endsWith

compareTo

contains

其他

 包装类

创建

常用方法

compareTo

equals

进制转换

数字字符串转成十进制

将十进制转成对应进制

StringBuilder

创建

字符串拼接

转成String

BigDecimal

创建

常用方法

泛型简介

自定义泛型

List

Set

Map


异常处理

程序在执行过程中会出现各种异常,例如一个水果超市里面只卖苹果和香蕉,但是客户的需求是买牛肉,这时就需要处理这个异常。一般来讲我们可以通过一个简单的操作来处理,比如:

String[] products = new String[]{"apple" , "banana"};
boolean findProduct(String s){
    for( int i = 0 ; i < products.length ; i ++ ){
        if( products[i].equals(s) )return true ;
    }
    return false ; 
}
void handle(){
    if( !findProduct("beef") ){
        System.out.println("没有这个商品"); // 处理异常
    }
}

不过,在Java中,本身就有专门处理异常的东西。使用Java专门的异常处理,可以防止程序在异常情况下持续执行,不论从可读性还是逻辑上都要优于上面的代码。Java异常处理的逻辑就是:抛出异常,捕获异常并进行处理,给出反馈。

捕获

异常的捕获通过catch语句实现,格式如下:

try{
    //可能出现异常的代码
}catch (异常的类型){
    //解决方法
}

可能出现异常的代码就是我们可能要处理的部分,如上面说到的购买物品时的方法。异常的类型是指我们try语句中遇到问题时所抛出的异常对象的类型。catch中的解决方法一般就是反馈问题等。例如在Integer类的方法parseInt中,如果我们给patseInt的参数并非数字字符,那么就无法将其转成整型,这就是一个异常,此时parseInt会抛出一个NumberFormatException类型的异常,我们可以通过对这个异常进行处理(捕获这个异常,并给出解决方法)来提示用户传参错误。就像这样:

public class Main {
    public static void main(String[] args){
        try{
            int number = Integer.parseInt("abcd");//abcd是非数字字符,不能转成数字,此时产生异常
            //这里parseInt会抛出NumberFormatException类型的异常:
            //throw new NumberFormatException();
            //这里的throw语句已经在parseInt中设计好,不需要我们专门再写
        }catch (NumberFormatException e){
            System.out.println("非数字字符不能转成数字哦");
        }
    }

}

输出: 

非数字字符不能转成数字哦 

抛出

抛出异常通过throw关键字抛出一个异常对象实体来实现,上面代码中的:

throw new NumberFormatException();

就是一个例子,它抛出了一个NumberFormatException异常类的实体,这个异常类是已经定义好的,我们也可以抛出自己定义的异常类的实体,不过需要注意的是,抛出的异常必须是实体,而且在异常抛出后,try语句块后面的代码不会再执行 ,紧接着执行catch语句块,这也防止了上文说到的“程序在异常情况下持续执行”,以免预料之外的错误。

自定义异常类

结合捕获和抛出,我们可以针对不同的异常情况,自己定义异常并进行处理(这里说的是自己定义异常,可不是自己制造异常哦)。所有的自定义的异常类都需要是Exception的子类,也就是说,我们在定义异常类时,需要继承Exception父类:

public class 异常名称 extends Exception{……}

将可能出现异常的方法用throws和异常类名绑定,当方法体中检测出异常时,就抛出对应的异常对象实体,如果不绑定,则需要通过try-catch语句抛出异常,否则无法抛出对应异常的实体:

void exampleMethod() throws 异常名称{……}

注意:通过throws和异常类绑定的方法才可以使用throw抛出异常,这样的方法需要放在try中,只有try内可以抛出异常。之后再通过catch语句捕获到对应类型的异常(捕获到对应异常类型的实体,参数是该实体的引用):

 catch(异常名称 变量名){……}

就可以灵活处理各种潜在的异常。就拿上面说到的水果超市购物为例:

//ProductTypeException异常类:
public class ProductTypeException extends Exception{
    private String message = "没有这个商品";
    public void warn(){
        System.out.println(message);
    }
}
//FruitShop类:
public class FruitShop {
    String[] products = new String[]{"apple" , "banana"};
    boolean findProduct(String s){
        for( int i = 0 ; i < products.length ; i ++ ){
            if( products[i].equals(s) )return true ;
        }
        return false ; // 处理异常
    }
    void purchase(String s) throws ProductTypeException{ //和异常类绑定的方法
        if( !findProduct(s) ){
            throw new ProductTypeException(); //方法内抛出对应异常实体
        }
        else {
            //……
        }
    }
    void purchaseWithoutThrows(String s){
        if( !findProduct(s) ){
            try {
                throw new ProductTypeException(); //通过try抛出异常实体
            }catch (ProductTypeException e){
                e.warn();
            }
        }
        else{
            //……
        }
    }
}
//Main类:
public class Main {
    public static void main(String[] args){
        FruitShop fruitShop = new FruitShop();

        try{
            fruitShop.purchase("beef");
        }catch (ProductTypeException e){
            e.warn();
        }

        fruitShop.purchaseWithoutThrows("beef");
    }
}

输出:

没有这个商品

没有这个商品 

 finally

try-catch语句除了try和catch,还可以有一个子语句——finally{……},格式如下:

try{
    //……
}catch(异常类名 变量){
    //……
}finally{
    //……
}

不论try中是否抛出异常,finally语句都会执行,但是有下面两种特殊情况:

1. try-catch语句内执行return语句后,finally仍会执行

2. 如果try-catch中执行了程序退出代码System.exit(0),则finally不会执行

 那finally语句有什么用呢?在某些情况,不论异常是否出现,有些语句一定要执行,就拿上面的水果超市举例,不管客户购买时是否出现异常,最后都要说一句“欢迎下次光临”(假设出现异常以后客户就不再买了),那么在我们处理完异常后就可以使用finally语句勇敢地对客户说出心中想说的那句话:

try{
    fruitShop.purchase("beef");
}catch (ProductTypeException e){
    e.warn();
}finally {
    System.out.println("欢迎下次光临");
}

其余代码同上。执行完这个try-catch后就会输出:

没有这个商品

欢迎下次光临

String

绝大多数语言中,字符串都是避不开的话题。C/C++中,我们用字符数组来处理字符串,也搭配了一系列的字符处理函数,而且C++还提供了string容器。而在Java中则又是一种全新的方式来专门处理字符串。首先就是常见的字符数组,但是Java的字符数组只是正儿八经的数组,它不支持直接被字符串赋值或初始化,只能一个字符一个字符地赋值:

char[] ch = new char[]{'o','h'};

就和其他类型的数组的操作方式一模一样。可见,Java的字符数组处理字符串貌似不是很方便。所以关于字符串,真正的主角是String。 

众所周知,String是引用数据类型,是一个封装好的类。

创建方法

String常用的构造方法(先不看构造方法体)有:

String(String s)

String str = new String(s);

创建一个指向s实体的对象变量str,即:将s的引用赋给新变量str

String(char[] c)

String str = new String(c);

将c内的所有字符拼接成字符串,并让新变量str指向该字符串实体(也就是将c复制一份赋给新变量str)

String(char[] c,int startIndex,int count)

String str = new String(c,startIndex,count);

过程同上,不过是把c中下标从startIndex开始后面的count个字符拼接成字符串,并让新变量str指向该实体

此外,就是所有人都心心念念的字符串直接赋值的事情,String是支持的!例如

String str = "It's supported!";

比较

String是引用数据类型,当比较两个String类时,如果像这样:

String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2);

如果单纯的用==运算符去比,这时输出的是false,也就是说,s1 != s2,因为String是引用数据类型,通过==运算符只是比较的s1和s2两个变量本身的值是否相等,也就是比的他们所存放的引用值是否相等,也就是比的他们是否指向同一个对象实体。Java的new运算符顾名思义,就是开辟新天体,即使s1和s2的内容一样,它们也都是通过开辟新天体得到的,此时这两个新天地是完全不同的两个地方,也就是它们指向的是两个实体。

虽然这样很合逻辑,但是很反人类,因为我们大多数时候只关心两个字符串内容,并不关心它们的引用值,这时想要比较s1和s2两个String变量的内容,则需要通过其中一个String对象调用equals(String s)方法来进行比较:

System.out.println( s1.equals(s2) );

这时输出true。

(注意是equals,不是equal哦,因为equals方法的调用者是一个字符串,是第三人称,谓语动词要变第三人称单数 [狗头] )

那么再考虑一种情况:

String s1 = "oi";
String s2 = "oi";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2))

先说结果:

true

true

这涉及到Java对于常量和变量的处理:

对于变量:变量存放在动态区。new就是在动态区中开辟新天地的,换言之,new创建出来的是变量,即使创建两个内容相同的东西,它们也在不同的内存块,引用值也是不同的。

对于常量:常量在常量池中。常量本身是不可变,每次创建出来常量以后,不会再创建新的内容相同的常量。对于构建出相同的字符串常量,它们是同一个东西,引用值也是相同的。

对于上面的情况,"oi"是在常量池中构建出来的常量,它具有一个引用值(假如是22FF),执行玩s1="oi"后,在执行s2="oi"时,由于常量池中已经有了“oi”常量,此时并不会创建新的“oi”常量,而是直接把现有的“oi”常量的引用值22FF赋给s2,所以s1和s2的引用值相等,内容也相等,它们指向的是同一个实体,这一点与new构建出来的变量不同。

拼接

Java的字符串允许+运算:把+后面的字符串接到前面的字符串上。下面是一个简单的栗子:

String str1 = "hel";
String str2 = "lo";
String string1 = str1 + str2 ;
String string2 = str1 + str2 ;
System.out.println( string1 == string2 );
System.out.println( string1.equals(string2) );

此时string1和string2的内容就是“hello”,那string1和string2是否==呢?在执行拼接操作时:

+左右两端都是常量:那么拼接后的新字符串也是按照常量进行构造,存放在常量池。

+左右两端不全是常量:那么拼接后的字符串按变量处理,存放在动态区。

上面的str1和str2存放着常量的引用,但它们本身是变量,此时str1+str2就是 变量+变量 ,此时产生的新字符串是一个动态区的变量,每次产生一个变量都会在动态区中开辟一块新内存,所以两次str1+str2产生的是两个不同的对象实体(不过它们内容相同)。上面程序输出:

false

true

常用方法

length

String对象调用length方法返回字符串的有效字符的个数,这句话是不是似曾相识,它和C/C++里的strlen函数的功能相同。用法如下:

String exampleStr = "Java太香啦";
System.out.println(exampleStr.length());
System.out.println("芜湖".length());

输出:

7

2

equals

用于比较两个字符串内容是否相同,很常用。上面说到的==一般不怎么用,通常我们只关注两个字符串的内容,并不关注它们指向的是不是同一个实体。equals的用法就是String对象调用equals(String s)方法和参数s字符串比较内容是否相同,上文已经有一些相关的例子。

starsWith和endsWith

原型是public boolean starsWith(String s),endsWith同理。顾名思义,它们用来比较某个String对象是否以参数s开始/结束:

String exampleStr = "abcdefg";
System.out.println(exampleStr.startsWith("abc"));
System.out.println(exampleStr.startsWith("bc"));
System.out.println(exampleStr.endsWith("efg"));
System.out.println(exampleStr.endsWith("oi"));

输出:

true

false

true

false

这两个方法是被重载过的方法,还有一种用法的原型是:public boolean startsWith(String s , int offset),意思是从offset的位置开始的字符串是否以s开头(即判断从offset开始到最后这个字串的前缀是否是s),注意是从下标为offset的位置开始,而不是第offset个位置,下标从0开始。endsWith没有这种重载,毕竟它比的是字符串的后缀,就没有从哪里开头这一说了。用法也很简单:

String exampleStr = "abcdefg";
System.out.println(exampleStr.startsWith("abc"));
System.out.println( exampleStr.startsWith("bcd",1) );
System.out.println( exampleStr.startsWith("cde",3) );

输出:

true

true

false

compareTo

原型是:public int compareTo(String s),这个方法用来比较两个字符串的字典序大小,如果调用者大于参数s,返回正数;小于返回负数;相等返回0。和C/C++的strcmp功能相同,用法如下:

String s1 = "abcdefg";
String s2 = "bcdae";
System.out.println(s1.compareTo(s2));
System.out.println(s1.compareTo(s1));
System.out.println(s2.compareTo(s1));

输出:

-1

0

1

有时在比较字母时,我们只关心是否是同一个字母,并不关心大小写是否相同,此时就可以用compareToIgnoreCase(String s)方法:

String s1 = "ABC";
String s2 = "abc";
System.out.println(s1.compareTo(s2));
System.out.println(s1.compareToIgnoreCase(s2));

输出:

-32

0

contains

原型是:public boolean contains(String s),用来判断参数s是否是调用者的子串,注意是子串,而不是子序列。这里需要小区分一下:

子串:在原串中截取一部分下来,子串在原串中一定是连续的一部分,例如abcde的子串有abc,bcde,bcd等。

子序列:在原串中按先后顺序取一些字符再合并到一起,子序列在原串中不一定连续,如abcde的子序列有ace,bde等,只要保证先后顺序不变就行。

contains的用法如下:

String s1 = "abcde";
System.out.println(s1.contains("bcd"));
System.out.println(s1.contains("bde"));//bde是子序列,但不是子串

输出:

true

false 

其他

其他的String的方法还有如:

int indexOf(String s):返回s子串在调用者中第一次出现的下标,如果调用者不含有s,则返回-1

int lastIndexOf(String s):用法同上,区别在于它返回的是最后一次出现的下标

String substring(int startpoint):返回调用者从下标startpoint开始到最后的子串

String substring(int start , int end):上面方法的重载,返回调用者下标在[start , end),即从start开始到end-1的子串

String trim():返回调用者去掉前后空格后的子串

还有各种基本类型转成字符串的方法,如int转成String的:static String valueOf(int n)类方法等。

在其他类型中还有一些与String相关的方法,例如将String转成对应数据的方法:

Integer中的parseInt(String s),还有其他对应基本类型的方法等。

 包装类

众所周知,Java有八种基本数据类型,而Java的核心思想是面向对象,这一点在基本数据类型上貌似体现的并不是很明显,而且对基本数据类型进行一些复杂的处理也很麻烦。Java对每种基本数据类型都提供了对应的包装类:Boolean, Byte, Short, Integer, Long, Float, Double, Character。这些包装类相当于基本数据类型的plus版本,它们都是引用数据类型,里面有很多实用方便的方法,使得我们在对基本数据类型处理时方便很多。由于这些包装类的各种方法实现的功能都大差不差,都是对对应的基本类型进行某种操作,所以就暂时拿Integer举例进行详细说明。

创建

一种是通过调用Integer的valueOf类方法,这个方法会返回一个Integer实例:

Integer x = Integer.valueOf(3) ;

 另一种是最方便的直接赋值:

Integer x = 3 ;

常用方法

compareTo

int compareTo(Integer anotherInteger),和String的compareTo相同调用者大于参数时返回正数,等于返回0,小于返回负数。

public class Main {
    public static void main(String[] args){
        Integer x = 3 ;
        System.out.println(x.compareTo(2));
        System.out.println(x.compareTo(3));
        System.out.println(x.compareTo(4));
    }
}

输出:

1

0

-1

equals

用于判断两个Integer内容是否相等,和String相同,这里不再赘述。

进制转换

各种包装类中,都有和String相互转换的方法,例如String转成Integer的parseInt(String s)方法,和Integer转成String的toString(Integer x)方法,而且这两种都是Interger的类方法,也都是被重载过的方法。那么和字符串的转换和我们的主题:进制转换有什么关联呢?

在存储数据时,当我们定义一个基本类型int x = 3 ; 此时在x所在的内存中存放的并非3本身,而是3对应的二进制数;在我们输入数据时,通过键盘输入的数字也并非数字本身,而是以字符串的形式读入到计算机。计算机的世界里只有0和1,但是我们在输出x时呈现给我们的时十进制数3本身,这是因为在输出时系统默认将二进制格式化为十进制进行显示。有了这个原理,我们就可以让系统将二进制格式化成我们想要的进制进行输出,而这一点正是可以通过将数据和字符串的转换的方法来实现。

数字字符串转成十进制

通过parseInt(String s , int x),按x进制进行解析,将数字字符串s转成对应数值,也就是说,我们给定的数字字符是x进制的,最后的返回值是十进制数据,但是存在内存中的永远都是二进制哦。

public class Main {
    public static void main(String[] args){
        Integer x = Integer.parseInt("F" , 16);
        System.out.println(x);
    }
}

输出:

15

如果我们给定的数字字符串和对应进制不匹配,则会抛出NumberFormatException异常,就像我们上面在异常处理中说到的一样。parseInt方法的x默认是十进制。

将十进制转成对应进制

通过toString(int number , int x)方法,将十进制数number转成x进制数对应的字符串表示,最终返回值是一个String,例如:

public class Main {
    public static void main(String[] args){
        System.out.println(Integer.toString(15,16));
    }
}

输出:

f

有没有发现我们介绍的进制转换只是针对十进制和其他进制转换的,这是因为计算机的默认格式化是十进制,数据的存放形式是二进制,如果我们要进行八进制转十六进制,无非也就是在计算机的格式化上做文章,也就是和十进制打交道,最后转成的对应进制只是显示的问题而已,而原来存放的数据还是它自己,并没有变成所谓的“对应进制”,就像下面这样:

public class Main {
    public static void main(String[] args){
        Integer x = 15 ;
        System.out.println(Integer.toString(x,16));
        System.out.println(x);
    }
}

输出:

f

15

虽然看起来是把x转成了十六进制,但只是在显示层面的一种转换而已,最后在计算机内存的还是那个二进制,最后一行输出x时还是按照默认十进制进行格式化输出,x并没有任何变化,所以我们刚刚说到的“八进制转成十六进制”的事情,可以说是不存在的事情,我们最后要打交道的还是针对x的默认格式化(十进制),计算机要面对的还是x的二进制,途中不涉及八进制,最后结果只是十六进制的一种呈现。

StringBuilder

我们在进行字符串拼接时,经常需要创建一个新的对象实体,然后计算引用再重新赋给变量,这样子效率比较低,StringBuilder类就解决了这个问题。

创建

StringBuilder常用的创建方法有两种,一种是在构造方法中传入一个int型参数,来指定数组的初始大小;另一种是直接传入一个字符串,此时StringBuilder类里面的内容就是这个字符串:

StringBuilder s1 = new StringBuilder(1000); //传入初始大小
StringBuilder s2 = new StringBuilder("hello"); //传入字符串内容

字符串拼接

StringBuilder的一个主要解决的就是字符串的拼接问题,它会预分配缓存区,这样在拼接时不需要再创建新的对象实体。StringBuilder的拼接通过append方法来实现。用法如下:

public class AboutStringBuilder {
    public static void main(String[] args){
        StringBuilder str = new StringBuilder("I'm ");
        String s = "Builder";
        str.append("String");
        str.append(s);
        System.out.println(str);
    }
}

输出:

I'm StringBuilder

可见,StringBuilder的append方法也是被重载过的,它既可以接收StringBuiler类型的参数,也可以接收String类型的参数,并且将它们拼接到原有字符串的后面。而且StringBuilder的append可以累加,就像这样:

public class AboutStringBuilder {
    public static void main(String[] args){
        StringBuilder str = new StringBuilder("I'm ");
        String s = "Builder";
        str.append("String").append(s); //进行累加
        System.out.println(str);
    }
}

那么这个是怎么实现的呢,通过StringBuilder的源码:

我们可以发现,每个append方法的返回值都是this,即它本身,因此实现了累加。 利用这个特性,我们可以写一个累加器,可以累加也可以累乘等:

public class Adder {
    private long sum = 0;
    Adder accumulate(int n){
        this.sum += n ;
        return this;
    }
    Adder multiply(int n){
        this.sum *= n ;
        return this;
    }
    long getSum(){
        return this.sum;
    }
}

public class MainClass {
    public static void main(String[] args){
        Adder adder = new Adder();
        adder.accumulate(3).accumulate(5).multiply(2);
        System.out.println(adder.getSum());

        Adder otherAdder = new Adder();
        otherAdder.accumulate(1).multiply(1).multiply(2).multiply(3); //3的阶乘
        System.out.println(otherAdder.getSum());
    }
}

输出:

16

转成String

虽然StringBuilder和String的用法都很像,但是StringBuilder终究不是String,有些时候我们需要把StringBuilder转成String对象,再调用String的相关方法进行某些操作,比如判断某个StringBuilder对象的内容是否包含Builder子串,我们可以这样:

public class AboutStringBuilder {
    public static void main(String[] args){
        StringBuilder str = new StringBuilder("I'm ");
        String s = "Builder";
        str.append("String");
        str.append(s);
        String string = str.toString(); //转成字符串
        System.out.println(string.contains("Builder")); //调用String的contains方法
    }
}

输出:

true 

BigDecimal

在程序设计中,经常会处理一些大到离谱的数字,而Java的math包中的BigDecimal类,它为浮点数的高精度处理提供了很多现成的方法。

创建

BigDecimal的创建方法由很多,可以把不同类型的数据放到构造方法的参数中,将其转成BigDecimal类型的数据。下面是用整型、String、和double型创建BigDecimal对象的栗子:

BigDecimal bd1 = new BigDecimal(120);
BigDecimal bd2 = new BigDecimal("12.5");
BigDecimal bd3 = BigDecimal.valueOf(25.5);

常用方法

既然是math包中的类,自然少不了各种数学上的运算的方法:

BigDecimal add(BigDecimal another):加

BigDecimal subtract(BigDecimal another):减

BigDecimal multiply(BigDecimal another):乘

BigDecimal divide(BigDecimal another):除,如果商是无限小数,则会抛出异常

BigDecimal divide(BigInteger other , RoundingMode mode):上面divide方法的重载,后面的RoundingMode类可以对小数进行取舍,如最常用的四舍五入:RoundingMode.HALE_UP

BigDecimal divide(BigInteger other , int scale , RoundingMode mode):同样是上述方法的重载,scale用于设定保留几位小数。

int compareTo(BigDecimal other):比较两个BigDecimal的大小,返回值和String的compareTo相同。

下面是一些简单的加减乘除的例子:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class AboutBigDecimal {
    public static void main(String[] args){
        BigDecimal bd1 = new BigDecimal("10.2");
        BigDecimal bd2 = BigDecimal.valueOf(3.3);
        System.out.println(bd1.add(bd2));
        System.out.println(bd1.subtract(bd2));
        System.out.println(bd1.multiply(bd2));
        System.out.println(bd1.divide(bd2,5, RoundingMode.HALF_UP)); //保留5位小数,四舍五入
        System.out.println(bd1.compareTo(bd2));
    }
}

输出:

13.5
6.9
33.66
3.09091
1

泛型简介

泛型,顾名思义,并不是某种具体的类型。它是一种数据集合的模板(框架),在我们对一些数据集合的处理上,针对不同的数据类型,因为它们类型不同,可能出现类型转换上的问题,所以我们一般只对同类型的集合进行处理,但是对于不同类型可能某些方法是相同的。听力来有点绕,举个栗子,假如我们有Integer和Double两种数据的集合,这两种集合都需要写一个增添元素的add方法,这时对于两种集合都要写类似的代码进行add,如果再有一个String集合,就又需要写一次add,而且Java有数不清的不同种的类,那么这些集合都要写一次add,而且还有自己定义的各种类……。这显然是很离谱的,而泛型编程就解决了这种问题,它提供了一套模板,不同的数据类型都可以用这套模板来打造集合,这时就不需要再反复的编写add等方法,可以直接用模板的方法。

自定义泛型

泛型的定义的语法是class 类名<类型>,这里的类型不可以是基本数据类型,也不是特定的类型,毕竟是泛型嘛,规定了类型就不能叫泛型了。这里的类型只是一个标识符,任何合法的标识符都可以放在这里,未来这里将接收不同的引用类型,在泛型的类体中可以使用这个标识符。定义方法如下:

class Example<T> {……}

这里的T可以接收任何引用类型,但是需要注意一点,T也是Object类的子类,可以调用Object类的方法。假如每个类都需要一个speak方法来输出一句话,这时就可以调用这个模板:

public class Example<T> {
    public void speak(){
        System.out.println("oi");
    }
}

然后创建各种对应的类型的对象,再调用speak方法:

public class Main {
    public static void main(String[] args){
        Example<String> e1 = new Example<>();//接收String类
        e1.speak();
        Example<Integer> e2 = new Example<>();//接收Integer类
        e2.speak();
        Example<Student> e3 = new Example<>();//接收自定义的Student类
        e3.speak();
        Example<Exception> e4 = new Example<>();//甚至是一个异常类
        e4.speak();
    }
}

 输出:

oi
oi
oi
oi

还可以定义泛型的集合,来存放多个数据:

public class Example<T> {
    Object[] array = new Object[100] ; //集合最大时100个元素
    private Integer total = 0 ;
    void add(T e){
        array[total] = e ;
        total ++ ;
    }
    int length(){
        return total ;
    }
    void output(){
        for( int i = 0 ; i < total ; i ++ ){
            System.out.print(array[i] + " ");
        }
        System.out.println();
    }
}

public class Main {
    public static void main(String[] args){
        Example<String> es = new Example<>();
        es.add("hello");
        es.add("world");
        es.add("!");
        System.out.println(es.length());
        es.output();
    }
}

输出:

3
hello world ! 

Java也有一些现成的泛型:list<T> , set<T> , 和 map<T>等。

List

List是列表,是一种泛型类的容器,它实现了collection接口,用来存放数据集合,它可以通过ArrayList或LinkedList或Vector实现。通常我们用ArrayList这个可变长的数组来实现:        

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args){
        List<Integer> li = new ArrayList<>()
    }
}

 list的常用方法有:

size():返回当前 list 的长度

add(ClassName val):在当前 list 尾部插入一个元素,这里的ClassName可以时任意类

add(int index, ClassName e):上面方法的重载,在当前 list 指定位置插入一个元素

get(int index):返回当前 list 中第 index 位置的值,若越界则抛出异常

set(int index, ClassName e):修改当前 list 中第 index 位置的值

还记得开头的那个水果超市嘛,咱们再拿它举个栗子:

import java.util.ArrayList;
import java.util.List;

public class Product {
    String name ;
    Double price ;
    Product(){}
    Product(String name , Double price){
        this.name = name ;
        this.price = price ;
    }
}

public class FruitShop {
    List<Product> list = new ArrayList<>();
    void addProduct(Product product){ //增添商品
        list.add(product);
    }
    int returnTypes(){ //返回一共有多少种商品
       return list.size();
    }
    Product getNthGoods(int index){ //获取第n个商品
        return list.get(index);
    }
    void setNthGoods(int index , Product product){ //修改第n个商品的信息
        list.set(index , product);
    }
} //感觉只是给list的各种方法改了个名……

public class Main {
    public static void main(String[] args){
        FruitShop fruitShop = new FruitShop();
        fruitShop.addProduct(new Product("apple" , 3.14));
        fruitShop.addProduct(new Product("banana" , 5.20));
        System.out.println("共有" + fruitShop.returnTypes() + "种商品");
        System.out.println("第1个水果是:" + fruitShop.getNthGoods(0).name + "价格:" + fruitShop.getNthGoods(0).price );
        fruitShop.setNthGoods(0 , new Product("applePlus" , 10.0 ));
        System.out.println("修改后:");
        System.out.println("第1个水果是:" + fruitShop.getNthGoods(0).name + "价格:" + fruitShop.getNthGoods(0).price );
        System.out.println("第1个水果是:" + fruitShop.getNthGoods(1).name + "价格:" + fruitShop.getNthGoods(1).price );
    }
}

输出:

共有2种商品
第1个水果是:apple价格:3.14
修改后:
第1个水果是:applePlus价格:10.0
第1个水果是:banana价格:5.2

Set

集合,它是保证内部元素不唯一的容器,可以通过HashSet、LinkedHashSet或TreeSet来实现,后两者是有序的,LinkedHashSet按插入顺序排序,TreeSet默认按照升序排列。我们拿有序的TreeSet来举例,既保证有序,有保证唯一,它的底层应该是红黑树,是不是似曾相识,C++的set容器和这里的TreeSet很像。下面是一个简单的栗子:

import java.util.*;

public class Main {
    public static void main(String[] args){
        Set<Integer> s1 = new HashSet<>(); // 无序
        Set<Integer> s2 = new LinkedHashSet<>(); // 按插入顺序排序
        Set<Integer> s = new TreeSet<>(); // 有序,默认升序
        //下面拿TreeSet的实现举例
        s.add(10);
        s.add(2);
        s.add(5);
        s.add(20);
        s.add(18);
        //此时s内:2 5 10 18 20
        for( Iterator<Integer> it = s.iterator() ; it.hasNext() ; ){
            System.out.println(it.next());
        } // 输出s
    }
}

输出:

2
5
10
18
20

既然是有序,那就可以自定义排序规则,对于我们自定义的排序规则,有两种方法可以让Set遵循这种规则:第一种是自然排序,在元素类型内部定义好比较规则(就像C++在结构体内重载operator<一样);第二种是用比较器进行比较(就像C++的set<int , cmp>一样)。这里我们暂时先用自然排序方法。

第一种的自定义排序,需要让元素的类去实现Comparable接口(Comparable<T>泛型),并重写compareTo方法。对于compareTo方法,如果返回值是负数,则把this的对象放在前面,如果是正数则放在右边,如果是0,则代表同一个元素,

以一个很经典的问题为例:假如我们有一个学生类,学生有姓名有成绩,按照成绩从大到小排序:

import java.util.*;

public class Student implements Comparable<Student>{
    String name = null ;
    Integer grade = null ;
    Student(){}
    Student(String name , Integer grade){
        this.name = name ;
        this.grade = grade ;
    }
    @Override
    public int compareTo(Student student) {
        if(this.grade != student.grade)
            return student.grade - this.grade ; //成绩高的放在前面
        else return this.name.compareTo(student.name); // 姓名小的放在前面
    } //此处必须写else一行,否则对于成绩相等的同学,会被认为是同一个元素
}

public class Main {
    public static void main(String[] args){
        Set<Student> s = new TreeSet<>();
        s.add(new Student("zhangsan",99));
        s.add(new Student("lisi",100));
        s.add(new Student("wangwu",98));
        s.add(new Student("zhaoliu",99));
        for( Student student : s ){
            System.out.println(student.name + " " + student.grade);
        }
    }
}

输出:

lisi 100
zhangsan 99
zhaoliu 99
wangwu 98 

注意:compareTo不止是排序规则,也是判定元素是否相同的标准!上面代码如果不写else一行,那么zhaoliu会被认为和zhangsan是同一个东西,不会被放到s中。

Set的常用方法有:

size():返回 Set 的元素个数

add(ClassName val):在Set种插入一个元素 

contains(ClassName val):判断 this 中是否有元素 val

remove(ClassName val):删除元素 val 

addAll(Collection e):将一个容器里的所有元素添加进 this

retainAll(Collection e):将 this 改为两个容器内相同的元素

removeAll(Collection e):将 this 中与 e 相同的元素删除

再举个栗子,还是那个水果店,我们把水果按名字排序摆放,再进行一些操作:

import java.util.*;

public class Product implements Comparable<Product>{
    String name ;
    Double price ;
    Product(){}
    Product(String name , Double price){
        this.name = name ;
        this.price = price ;
    }

    @Override
    public int compareTo(Product product) {
        return this.name.compareTo(product.name);
    } //将名称按字典序排序
}

public class FruitShop {
    Set<Product> set = new TreeSet<>();
    void addProduct(Product product){
        set.add(product);
    }
    int returnTypes(){
        return set.size();
    }
    boolean contains(Product product){
        return set.contains(product);
    }
}

public class Main {
    public static void main(String[] args){
        FruitShop fruitShop = new FruitShop();
        fruitShop.addProduct(new Product("apple" , 3.14));
        fruitShop.addProduct(new Product("banana" , 5.20));
        System.out.println("初始");
        for(Product product : fruitShop.set){
            System.out.println(product.name + " " + product.price);
        }

        Set<Product> trunk = new TreeSet<>(); //进货
        trunk.add(new Product("mango",5.8));
        trunk.add(new Product("pear",4.5));
        fruitShop.set.addAll(trunk);
        System.out.println("进货后");
        for(Product product : fruitShop.set){
            System.out.println(product.name + " " + product.price);
        }

        Set<Product> buyer = new TreeSet<>(); //大买家
        buyer.add(new Product("banana" , 5.20));
        buyer.add(new Product("pear",4.5)); //买光所有的banana和pear
        fruitShop.set.removeAll(buyer);
        System.out.println("购买后");
        for(Product product : fruitShop.set){
            System.out.println(product.name + " " + product.price);
        }
    }
}

输出:

初始
apple 3.14
banana 5.2
进货后
apple 3.14
banana 5.2
mango 5.8
pear 4.5
购买后
apple 3.14
mango 5.8

Map

映射,和C++的map类似,每个元素是key和value的键值对,可以通过HashMap、LinkedHashMap或TreeMap,与Set类似,HashMap无序,LinkedHashMap按插入顺序排列,TreeMap默认按照key升序排列。

它的常用方法有:

put(ClassName key, ClassName value):插入一个元素

size():返回 map 的长度

containsKey(ClassName val):判断 map 中是否有元素 key 为 val

get(ClassName key):将 map 中对应的 key 的 value 返回

keySet():将 map 中所有元素的 key 作为集合返回

接下来是不是该我们的水果店上场了呢,一个商品有名称有价格,就可以看出是名称到价格的一种映射,这不拿它举栗子拿谁举呢:

import java.util.*;

public class Main {
    public static void main(String[] args){
        // 我们还是以默认按key升序的TreeMap举例
        Map<String , Double> fruitShop = new TreeMap<>();
        fruitShop.put("apple" , 3.14);
        fruitShop.put("banana" , 5.20);
        System.out.println("店里有:");
        for( String key : fruitShop.keySet() ){
            System.out.println(key + " " + fruitShop.get(key));
        }
        System.out.println("一共" + fruitShop.size() + "种水果");

        if(fruitShop.containsKey("mango")) System.out.println("有mango");
        else System.out.println("没有mango");

        if(fruitShop.containsValue(3.14)) System.out.println("有3.14元的水果");
        else System.out.println("没有3.14元的水果");
    }
}

输出:

店里有:
apple 3.14
banana 5.2
一共2种水果
没有mango
有3.14元的水果

  • 27
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值