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

8159 字
41 分钟
Java异常处理+常用类+泛型

异常处理#

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

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
6

转成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类可以对小数进行取舍,如最常用的四舍五入.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 , set , 和 map等。

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个水果是价格:3.14
修改后:
第1个水果是价格:10.0
第1个水果是价格: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泛型),并重写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元的水果

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Java异常处理+常用类+泛型
https://blog.cplee.cn/posts/java-exception-common-generics
作者
cplee
发布于
2024-03-31
许可协议
CC BY-NC-SA 4.0
最后更新于 2024-03-31,距今已过 793 天

部分内容可能已过时

评论区

Profile Image of the Author
cplee
Hello, I'm cplee.
公告
欢迎来到我的博客!点击下方 “了解更多” 查看更多信息
分类
标签
站点统计
文章
6
分类
3
标签
9
总字数
31,269
运行时长
0
最后活动
0 天前

目录