关于java:基础2

5次阅读

共计 24867 个字符,预计需要花费 63 分钟才能阅读完成。

异样解决

Java 的异样

在计算机程序运行的过程中,总是会呈现各种各样的谬误。

有一些谬误是用户造成的,比方,心愿用户输出一个 int 类型的年龄,然而用户的输出是abc

// 假如用户输出了 abc:String s = "abc";
int n = Integer.parseInt(s); // NumberFormatException!

程序想要读写某个文件的内容,然而用户曾经把它删除了:

// 用户删除了该文件:String t = readFile("C:\\abc.txt"); // FileNotFoundException!

Java 内置了一套异样解决机制,总是应用异样来示意谬误。

异样是一种class,因而它自身带有类型信息。异样能够在任何中央抛出,但只须要在下层捕捉,这样就和办法调用拆散了:

try {String s = processFile(“C:\\test.txt”);
    // ok:
} catch (FileNotFoundException e) {// file not found:} catch (SecurityException e) {// no read permission:} catch (IOException e) {// io error:} catch (Exception e) {// other error:}

因为 Java 的异样是class,它的继承关系如下:

                     ┌───────────┐
                     │  Object   │
                     └───────────┘
                           ▲
                           │
                     ┌───────────┐
                     │ Throwable │
                     └───────────┘
                           ▲
                 ┌─────────┴─────────┐
                 │                   │
           ┌───────────┐       ┌───────────┐
           │   Error   │       │ Exception │
           └───────────┘       └───────────┘
                 ▲                   ▲
         ┌───────┘              ┌────┴──────────┐
         │                      │               │
┌─────────────────┐    ┌─────────────────┐┌───────────┐
│OutOfMemoryError │... │RuntimeException ││IOException│...
└─────────────────┘    └─────────────────┘└───────────┘
                                ▲
                    ┌───────────┴─────────────┐
                    │                         │
         ┌─────────────────────┐ ┌─────────────────────────┐
         │NullPointerException │ │IllegalArgumentException │...
         └─────────────────────┘ └─────────────────────────┘

从继承关系可知:Throwable是异样体系的根,它继承自 ObjectThrowable 有两个体系:ErrorExceptionError 示意重大的谬误,程序对此个别无能为力,例如:

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无奈加载某个 Class
  • StackOverflowError:栈溢出

Exception 则是运行时的谬误,它能够被捕捉并解决。

某些异样是利用程序逻辑解决的一部分,应该捕捉并解决。例如:

  • NumberFormatException:数值类型的格局谬误
  • FileNotFoundException:未找到文件
  • SocketException:读取网络失败

还有一些异样是程序逻辑编写不对造成的,应该修复程序自身。例如:

  • NullPointerException:对某个 null 的对象调用办法或字段
  • IndexOutOfBoundsException:数组索引越界

Exception又分为两大类:

  1. RuntimeException以及它的子类;
  2. RuntimeException(包含IOExceptionReflectiveOperationException 等等)

Java 规定:

  • 必须捕捉的异样,包含 Exception 及其子类,但不包含 RuntimeException 及其子类,这种类型的异样称为 Checked Exception。
  • 不须要捕捉的异样,包含 Error 及其子类,RuntimeException及其子类。

捕捉异样

捕捉异样应用 try...catch 语句,把可能产生异样的代码放到 try {...} 中,而后应用 catch 捕捉对应的 Exception 及其子类

public class Main {public static void main(String[] args) {byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }

    static byte[] toGBK(String s) {
        try {// 用指定编码转换 String 为 byte[]:
            return s.getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            // 如果零碎不反对 GBK 编码,会捕捉到 UnsupportedEncodingException:
            System.out.println(e); // 打印异样信息
            return s.getBytes(); // 尝试应用用默认编码}
    }
}

如果咱们不捕捉UnsupportedEncodingException,会呈现编译失败的问题

编译器会报错,错误信息相似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,并且精确地指出须要捕捉的语句是 return s.getBytes("GBK");。意思是说,像UnsupportedEncodingException 这样的 Checked Exception,必须被捕捉。

这是因为 String.getBytes(String) 办法定义是:

public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {...}

在办法定义的时候,应用 throws Xxx 示意该办法可能抛出的异样类型。调用方在调用的时候,必须强制捕捉这些异样,否则编译器会报错。

小结

Java 应用异样来示意谬误,并通过 try ... catch 捕捉异样;

Java 的异样是 class,并且从Throwable 继承;

Error是无需捕捉的严重错误,Exception是应该捕捉的可解决的谬误;

RuntimeException无需强制捕捉,非 RuntimeException(Checked Exception)需强制捕捉,或者用throws 申明;

不举荐捕捉了异样但不进行任何解决。

捕捉异样

在 Java 中,但凡可能抛出异样的语句,都能够用 try ... catch 捕捉。把可能产生异样的语句放在 try {...} 中,而后应用 catch 捕捉对应的 Exception 及其子类。

多 catch 语句

能够应用多个 catch 语句,每个 catch 别离捕捉对应的 Exception 及其子类。JVM 在捕捉到异样后,会从上到下匹配 catch 语句,匹配到某个 catch 后,执行 catch 代码块,而后_不再_持续匹配。

简略地说就是:多个 catch 语句只有一个能被执行。例如:

public static void main(String[] args) {
    try {process1();
        process2();
        process3();} catch (IOException e) {System.out.println(e);
    } catch (NumberFormatException e) {System.out.println(e);
    }
}

存在多个 catch 的时候,catch的程序十分重要:子类必须写在后面。例如:

public static void main(String[] args) {
    try {process1();
        process2();
        process3();} catch (IOException e) {System.out.println("IO error");
    } catch (UnsupportedEncodingException e) { // 永远捕捉不到
        System.out.println("Bad encoding");
    }
}

对于下面的代码,UnsupportedEncodingException异样是永远捕捉不到的,因为它是 IOException 的子类。当抛出 UnsupportedEncodingException 异样时,会被 catch (IOException e) {...} 捕捉并执行。

finally语句块保障有无谬误都会执行。上述代码能够改写如下:

public static void main(String[] args) {
    try {process1();
        process2();
        process3();} catch (UnsupportedEncodingException e) {System.out.println("Bad encoding");
    } catch (IOException e) {System.out.println("IO error");
    } finally {System.out.println("END");
    }
}

抛出异样

异样的流传

当某个办法抛出了异样时,如果以后办法没有捕捉异样,异样就会被抛到下层调用办法,直到遇到某个 try ... catch 被捕捉为止:

public class Main {public static void main(String[] args) {
        try {process1();
        } catch (Exception e) {e.printStackTrace();
        }
    }

    static void process1() {process2();
    }

    static void process2() {Integer.parseInt(null); // 会抛出 NumberFormatException
    }
}

通过 printStackTrace() 能够打印出办法的调用栈,相似:

java.lang.NumberFormatException: null
    at java.base/java.lang.Integer.parseInt(Integer.java:614)
    at java.base/java.lang.Integer.parseInt(Integer.java:770)
    at Main.process2(Main.java:16)
    at Main.process1(Main.java:12)
    at Main.main(Main.java:5)

printStackTrace()对于调试谬误十分有用,上述信息示意:NumberFormatException是在 java.lang.Integer.parseInt 办法中被抛出的,从下往上看,调用档次顺次是:

  1. main()调用process1()
  2. process1()调用process2()
  3. process2()调用Integer.parseInt(String)
  4. Integer.parseInt(String)调用Integer.parseInt(String, int)

查看 Integer.java 源码可知,抛出异样的办法代码如下:

public static int parseInt(String s, int radix) throws NumberFormatException {if (s == null) {throw new NumberFormatException("null");
    }
    ...
}

并且,每层调用均给出了源代码的行号,可间接定位。

抛出异样

当产生谬误时,例如,用户输出了非法的字符,咱们就能够抛出异样。

如何抛出异样?参考 Integer.parseInt() 办法,抛出异样分两步:

  1. 创立某个 Exception 的实例;
  2. throw 语句抛出。

上面是一个例子:

void process2(String s) {if (s==null) {NullPointerException e = new NullPointerException();
        throw e;
    }
}

实际上,绝大部分抛出异样的代码都会合并写成一行:

void process2(String s) {if (s==null) {throw new NullPointerException();
    }
}

如果一个办法捕捉了某个异样后,又在 catch 子句中抛出新的异样,就相当于把抛出的异样类型“转换”了:

void process1(String s) {
    try {process2();
    } catch (NullPointerException e) {throw new IllegalArgumentException();
    }
}

void process2(String s) {if (s==null) {throw new NullPointerException();
    }
}

自定义异样

Java 规范库定义的罕用异样包含:

Exception
│
├─ RuntimeException
│  │
│  ├─ NullPointerException
│  │
│  ├─ IndexOutOfBoundsException
│  │
│  ├─ SecurityException
│  │
│  └─ IllegalArgumentException
│     │
│     └─ NumberFormatException
│
├─ IOException
│  │
│  ├─ UnsupportedCharsetException
│  │
│  ├─ FileNotFoundException
│  │
│  └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException

当咱们在代码中须要抛出异样时,尽量应用 JDK 已定义的异样类型。例如,参数查看不非法,应该抛出IllegalArgumentException

static void process1(int age) {if (age <= 0) {throw new IllegalArgumentException();
    }
}

在一个大型项目中,能够自定义新的异样类型,然而,放弃一个正当的异样继承体系是十分重要的。

一个常见的做法是自定义一个 BaseException 作为“根异样”,而后,派生出各种业务类型的异样。

BaseException须要从一个适宜的 Exception 派生,通常倡议从 RuntimeException 派生:

public class BaseException extends RuntimeException {}

其余业务类型的异样就能够从 BaseException 派生:

public class UserNotFoundException extends BaseException {
}

public class LoginFailedException extends BaseException {
}

...

自定义的 BaseException 应该提供多个构造方法:

public class BaseException extends RuntimeException {public BaseException() {super();
    }

    public BaseException(String message, Throwable cause) {super(message, cause);
    }

    public BaseException(String message) {super(message);
    }

    public BaseException(Throwable cause) {super(cause);
    }
}

上述构造方法实际上都是原样照抄RuntimeException。这样,抛出异样的时候,就能够抉择适合的构造方法。通过 IDE 能够依据父类疾速生成子类的构造方法。

小结

抛出异样时,尽量复用 JDK 已定义的异样类型;

自定义异样体系时,举荐从 RuntimeException 派生“根异样”,再派生出业务异样;

自定义异样时,应该提供多种构造方法。

NullPointerException

在所有的 RuntimeException 异样中,Java 程序员最相熟的恐怕就是 NullPointerException 了。

NullPointerException即空指针异样,俗称 NPE。如果一个对象为null,调用其办法或拜访其字段就会产生NullPointerException,这个异样通常是由 JVM 抛出的,例如:

public class Main {public static void main(String[] args) {
        String s = null;
        System.out.println(s.toLowerCase());
    }
}

指针这个概念实际上源自 C 语言,Java 语言中并无指针。咱们定义的变量实际上是援用,Null Pointer 更确切地说是 Null Reference,不过两者区别不大。

解决 NullPointerException

如果遇到 NullPointerException,咱们应该如何解决?首先,必须明确,NullPointerException 是一种代码逻辑谬误,遇到 NullPointerException,遵循原则是早裸露,早修复,严禁应用catch 来暗藏这种编码谬误:

// 谬误示例: 捕捉 NullPointerException
try {transferMoney(from, to, amount);
} catch (NullPointerException e) {}

好的编码习惯能够极大地升高 NullPointerException 的产生,例如:

成员变量在定义时初始化:

public class Person {private String name = "";}

应用空字符串 "" 而不是默认的 null 可防止很多 NullPointerException,编写业务逻辑时,用空字符串"" 示意未填写比 null 平安得多。

返回空字符串""、空数组而不是null

public String[] readLinesFromFile(String file) {if (getFileSize(file) == 0) {
        // 返回空数组而不是 null:
        return new String[0];
    }
    ...
}

这样能够使得调用方无需查看后果是否为null

应用断言

断言(Assertion)是一种调试程序的形式。在 Java 中,应用 assert 关键字来实现断言。

咱们先看一个例子:

public static void main(String[] args) {double x = Math.abs(-123.45);
    assert x >= 0;
    System.out.println(x);
}

语句 assert x >= 0; 即为断言,断言条件 x >= 0 预期为true。如果计算结果为false,则断言失败,抛出AssertionError

应用 assert 语句时,还能够增加一个可选的断言音讯:

assert x >= 0 : "x must >= 0";

这样,断言失败的时候,AssertionError会带上音讯x must >= 0,更加便于调试。

Java 断言的特点是:断言失败时会抛出AssertionError,导致程序完结退出。因而,断言不能用于可复原的程序谬误,只应该用于开发和测试阶段。

对于可复原的程序谬误,不应该应用断言。例如:

void sort(int[] arr) {assert arr != null;}

应该抛出异样并在下层捕捉:

void sort(int[] arr) {if (x == null) {throw new IllegalArgumentException("array cannot be null");
    }
}

小结

断言是一种调试形式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言;

对可复原的谬误不能应用断言,而应该抛出异样;

断言很少被应用,更好的办法是编写单元测试。

应用 JDK Logging

在编写程序的过程中,发现程序运行后果与预期不符,怎么办?当然是用 System.out.println() 打印出执行过程中的某些变量,察看每一步的后果与代码逻辑是否合乎,而后有针对性地批改代码。

代码改好了怎么办?当然是删除没有用的 System.out.println() 语句了。

如果改代码又改出问题怎么办?再加上System.out.println()

重复这么搞几次,很快大家就发现应用 System.out.println() 十分麻烦。

怎么办?

解决办法是应用日志。

那什么是日志?日志就是 Logging,它的目标是为了取代System.out.println()

输入日志,而不是用System.out.println(),有以下几个益处:

  1. 能够设置输入款式,防止本人每次都写"ERROR:" + var
  2. 能够设置输入级别,禁止某些级别输入。例如,只输入谬误日志;
  3. 能够被重定向到文件,这样能够在程序运行完结后查看日志;
  4. 能够按包名管制日志级别,只输入某些包打的日志;
  5. 能够……

因为 Java 规范库内置了日志包java.util.logging,咱们能够间接用。先看一个简略的例子:

public class Hello {public static void main(String[] args) {Logger logger = Logger.getGlobal();
        logger.info("start process...");
        logger.warning("memory is running out...");
        logger.fine("ignored.");
        logger.severe("process will be terminated...");
    }
}

运行上述代码,失去相似如下的输入:

Mar 02, 2019 6:32:13 PM Hello main
INFO: start process...
Mar 02, 2019 6:32:13 PM Hello main
WARNING: memory is running out...
Mar 02, 2019 6:32:13 PM Hello main
SEVERE: process will be terminated...

比照可见,应用日志最大的益处是,它主动打印了工夫、调用类、调用办法等很多有用的信息。

再仔细观察发现,4 条日志,只打印了 3 条,logger.fine()没有打印。这是因为,日志的输入能够设定级别。JDK 的 Logging 定义了 7 个日志级别,从重大到一般:

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

因为默认级别是 INFO,因而,INFO 级别以下的日志,不会被打印进去。应用日志级别的益处在于,调整级别,就能够屏蔽掉很多调试相干的日志输入。

应用 Commons Logging

和 Java 规范库提供的日志不同,Commons Logging 是一个第三方日志库,它是由 Apache 创立的日志模块。

Commons Logging 的特色是,它能够挂接不同的日志零碎,并通过配置文件指定挂接的日志零碎。默认状况下,Commons Loggin 主动搜寻并应用 Log4j(Log4j 是另一个风行的日志零碎),如果没有找到 Log4j,再应用 JDK Logging。

反射

什么是反射?

反射就是 Reflection,Java 的反射是指程序在运行期能够拿到一个对象的所有信息。

失常状况下,如果咱们要调用一个对象的办法,或者拜访一个对象的字段,通常会传入对象实例:

// Main.java
import com.itranswarp.learnjava.Person;

public class Main {String getFullName(Person p) {return p.getFirstName() + " " + p.getLastName();}
}

然而,如果不能取得 Person 类,只有一个 Object 实例,比方这样:

String getFullName(Object obj) {return ???}

怎么办?有童鞋会说:强制转型啊!

String getFullName(Object obj) {Person p = (Person) obj;
    return p.getFirstName() + " " + p.getLastName();
}

强制转型的时候,你会发现一个问题:编译下面的代码,依然须要援用 Person 类。不然,去掉 import 语句,你看能不能编译通过?

所以,反射是为了解决在运行期,对某个实例无所不知的状况下,如何调用其办法。

Class 类

JVM 为每个加载的 classinterface创立了对应的 Class 实例来保留 classinterface的所有信息;
获取一个 class 对应的 Class 实例后,就能够获取该 class 的所有信息;
通过 Class 实例获取 class 信息的办法称为反射(Reflection);
JVM 总是动静加载class,能够在运行期依据条件来管制加载 class。

拜访字段

Java 的反射 API 提供的 Field 类封装了字段的所有信息:
通过 Class 实例的办法能够获取 Field 实例:getField()getFields()getDeclaredField()getDeclaredFields()
通过 Field 实例能够获取字段信息:getName()getType()getModifiers()
通过 Field 实例能够读取或设置某个对象的字段,如果存在拜访限度,要首先调用 setAccessible(true) 来拜访非 public 字段。
通过反射读写字段是一种非常规办法,它会毁坏对象的封装。

调用办法

当咱们获取到一个 Method 对象时,就能够对它进行调用。咱们以上面的代码为例:

String s = "Hello world";
String r = s.substring(6); // "world"

如果用反射来调用 substring 办法,须要以下代码:

 // String 对象:
String s = "Hello world";
// 获取 String substring(int)办法,参数为 int:
Method m = String.class.getMethod("substring", int.class);
// 在 s 对象上调用该办法并获取后果:
String r = (String) m.invoke(s, 6);
// 打印调用后果:
System.out.println(r);

Java 的反射 API 提供的 Method 对象封装了办法的所有信息:
通过 Class 实例的办法能够获取 Method 实例:getMethod()getMethods()getDeclaredMethod()getDeclaredMethods()
通过 Method 实例能够获取办法信息:getName()getReturnType()getParameterTypes()getModifiers()
通过 Method 实例能够调用某个对象的办法:Object invoke(Object instance, Object... parameters)
通过设置 setAccessible(true) 来拜访非 public 办法;
通过反射调用办法时,依然遵循多态准则。

调用构造方法

咱们通常应用 new 操作符创立新的实例:

Person p = new Person();

如果通过反射来创立新的实例,能够调用 Class 提供的 newInstance()办法:

Person p = Person.class.newInstance();

Constructor对象封装了构造方法的所有信息;

通过 Class 实例的办法能够获取 Constructor 实例:getConstructor()getConstructors()getDeclaredConstructor()getDeclaredConstructors()

通过 Constructor 实例能够创立一个实例对象:newInstance(Object... parameters);通过设置 setAccessible(true) 来拜访非 public 构造方法。

注解

应用注解

注解是放在 Java 源码的类、办法、字段、参数前的一种非凡“正文”:

// this is a component:
@Resource("hello")
public class Hello {
    @Inject
    int n;

    @PostConstruct
    public void hello(@Param String name) {System.out.println(name);
    }

    @Override
    public String toString() {return "Hello";}
}

正文会被编译器间接疏忽,注解则能够被编译器打包进入 class 文件,因而,注解是一种用作标注的“元数据”。

注解的作用

从 JVM 的角度看,注解自身对代码逻辑没有任何影响,如何应用注解齐全由工具决定。

Java 的注解能够分为三类:

第一类是由编译器应用的注解,例如:

  • @Override:让编译器查看该办法是否正确地实现了覆写;
  • @SuppressWarnings:通知编译器疏忽此处代码产生的正告。

这类注解不会被编译进入 .class 文件,它们在编译后就被编译器扔掉了。

第二类是由工具解决 .class 文件应用的注解,比方有些工具会在加载 class 的时候,对 class 做动静批改,实现一些非凡的性能。这类注解会被编译进入 .class 文件,但加载完结后并不会存在于内存中。这类注解只被一些底层库应用,个别咱们不用本人解决。

第三类是在程序运行期可能读取的注解,它们在加载后始终存在于 JVM 中,这也是最罕用的注解。例如,一个配置了 @PostConstruct 的办法会在调用构造方法后主动被调用(这是 Java 代码读取该注解实现的性能,JVM 并不会辨认该注解)。

定义一个注解时,还能够定义配置参数。配置参数能够包含:

  • 所有根本类型;
  • String;
  • 枚举类型;
  • 根本类型、String、Class 以及枚举的数组。

因为配置参数必须是常量,所以,上述限度保障了注解在定义时就曾经确定了每个参数的值。

注解的配置参数能够有默认值,短少某个配置参数时将应用默认值。

此外,大部分注解会有一个名为 value 的配置参数,对此参数赋值,能够只写常量,相当于省略了 value 参数。

如果只写注解,相当于全副应用默认值。

举个栗子,对以下代码:

public class Hello {@Check(min=0, max=100, value=55)
    public int n;

    @Check(value=99)
    public int p;

    @Check(99) // @Check(value=99)
    public int x;

    @Check
    public int y;
}

@Check就是一个注解。第一个 @Check(min=0, max=100, value=55) 明确定义了三个参数,第二个 @Check(value=99) 只定义了一个 value 参数,它实际上和 @Check(99) 是齐全一样的。最初一个 @Check 示意所有参数都应用默认值。

小结

注解(Annotation)是 Java 语言用于工具解决的标注:

注解能够配置参数,没有指定配置的参数应用默认值;

如果参数名称是value,且只有一个参数,那么能够省略参数名称。

定义注解

Java 语言应用 @interface 语法来定义注解(Annotation),它的格局如下:

public @interface Report {int type() default 0;
    String level() default "info";
    String value() default "";}

注解的参数相似无参数办法,能够用 default 设定一个默认值(强烈推荐)。最罕用的参数该当命名为value

元注解

元注解

有一些注解能够润饰其余注解,这些注解就称为元注解(meta annotation)。Java 规范库曾经定义了一些元注解,咱们只须要应用元注解,通常不须要本人去编写元注解。

@Target

最罕用的元注解是 @Target。应用@Target 能够定义 Annotation 可能被利用于源码的哪些地位:

  • 类或接口:ElementType.TYPE
  • 字段:ElementType.FIELD
  • 办法:ElementType.METHOD
  • 构造方法:ElementType.CONSTRUCTOR
  • 办法参数:ElementType.PARAMETER

例如,定义注解 @Report 可用在办法上,咱们必须增加一个@Target(ElementType.METHOD)

@Target(ElementType.METHOD)
public @interface Report {int type() default 0;
    String level() default "info";
    String value() default "";}

定义注解 @Report 可用在办法或字段上,能够把 @Target 注解参数变为数组{ElementType.METHOD, ElementType.FIELD}

@Target({
    ElementType.METHOD,
    ElementType.FIELD
})
public @interface Report {...}

实际上 @Target 定义的 valueElementType[]数组,只有一个元素时,能够省略数组的写法。

@Retention

另一个重要的元注解 @Retention 定义了 Annotation 的生命周期:

  • 仅编译期:RetentionPolicy.SOURCE
  • 仅 class 文件:RetentionPolicy.CLASS
  • 运行期:RetentionPolicy.RUNTIME

如果 @Retention 不存在,则该 Annotation 默认为 CLASS。因为通常咱们自定义的Annotation 都是 RUNTIME,所以,务必要加上@Retention(RetentionPolicy.RUNTIME) 这个元注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Report {int type() default 0;
    String level() default "info";
    String value() default "";}

@Repeatable

应用 @Repeatable 这个元注解能够定义 Annotation 是否可反复。这个注解利用不是特地宽泛。

@Inherited

应用 @Inherited 定义子类是否可继承父类定义的 Annotation@Inherited 仅针对 @Target(ElementType.TYPE) 类型的 annotation 无效,并且仅针对 class 的继承,对 interface 的继承有效:

@Inherited
@Target(ElementType.TYPE)
public @interface Report {int type() default 0;
    String level() default "info";
    String value() default "";}

在应用的时候,如果一个类用到了@Report

@Report(type=1)
public class Person {}

则它的子类默认也定义了该注解:

public class Student extends Person {}

如何定义 Annotation

咱们总结一下定义 Annotation 的步骤:

第一步,用 @interface 定义注解:

public @interface Report {}

第二步,增加参数、默认值:

public @interface Report {int type() default 0;
    String level() default "info";
    String value() default "";}

把最罕用的参数定义为value(),举荐所有参数都尽量设置默认值。

第三步,用元注解配置注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {int type() default 0;
    String level() default "info";
    String value() default "";}

其中,必须设置 @Target@Retention@Retention个别设置为 RUNTIME,因为咱们自定义的注解通常要求在运行期读取。个别状况下,不用写@Inherited@Repeatable

小结

Java 应用 @interface 定义注解:

可定义多个参数和默认值,外围参数应用 value 名称;

必须设置 @Target 来指定 Annotation 能够利用的范畴;

该当设置 @Retention(RetentionPolicy.RUNTIME) 便于运行期读取该Annotation

解决注解

能够在运行期通过反射读取 RUNTIME 类型的注解,留神千万不要漏写@Retention(RetentionPolicy.RUNTIME),否则运行期无奈读取到该注解。

能够通过程序处理注解来实现相应的性能:

  • 对 JavaBean 的属性值按规定进行查看;
  • JUnit 会主动运行 @Test 标记的测试方法。

注解如何应用,齐全由程序本人决定。例如,JUnit 是一个测试框架,它会主动运行所有标记为 @Test 的办法。

咱们来看一个 @Range 注解,咱们心愿用它来定义一个 String 字段的规定:字段长度满足 @Range 的参数定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {int min() default 0;
    int max() default 255;}

在某个 JavaBean 中,咱们能够应用该注解:

public class Person {@Range(min=1, max=20)
    public String name;

    @Range(max=10)
    public String city;
}

然而,定义了注解,自身对程序逻辑没有任何影响。咱们必须本人编写代码来应用注解。这里,咱们编写一个 Person 实例的查看办法,它能够查看 Person 实例的 String 字段长度是否满足 @Range 的定义:

void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
    // 遍历所有 Field:
    for (Field field : person.getClass().getFields()) {
        // 获取 Field 定义的 @Range:
        Range range = field.getAnnotation(Range.class);
        // 如果 @Range 存在:
        if (range != null) {
            // 获取 Field 的值:
            Object value = field.get(person);
            // 如果值是 String:
            if (value instanceof String) {String s = (String) value;
                // 判断值是否满足 @Range 的 min/max:
                if (s.length() < range.min() || s.length() > range.max()) {throw new IllegalArgumentException("Invalid field:" + field.getName());
                }
            }
        }
    }
}

这样一来,咱们通过 @Range 注解,配合 check() 办法,就能够实现 Person 实例的查看。留神查看逻辑齐全是咱们本人编写的,JVM 不会主动给注解增加任何额定的逻辑。

泛型

泛型是一种“代码模板”,能够用一套代码套用各种类型。

什么是泛型

在解说什么是泛型之前,咱们先察看 Java 规范库提供的ArrayList,它能够看作“可变长度”的数组,因为用起来比数组更不便。

实际上 ArrayList 外部就是一个 Object[] 数组,配合存储一个以后调配的长度,就能够充当“可变数组”:

public class ArrayList {private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

如果用上述 ArrayList 存储 String 类型,会有这么几个毛病:

  • 须要强制转型;
  • 不不便,易出错。

例如,代码必须这么写:

ArrayList list = new ArrayList();
list.add("Hello");
// 获取到 Object,必须强制转型为 String:
String first = (String) list.get(0);

很容易呈现 ClassCastException,因为容易“误转型”:

list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);

要解决上述问题,咱们能够为 String 独自编写一种ArrayList

public class StringArrayList {private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...}
}

这样一来,存入的必须是String,取出的也肯定是String,不须要强制转型,因为编译器会强制查看放入的类型:

StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译谬误: 不容许放入非 String 类型:
list.add(new Integer(123));

问题临时解决。

然而,新的问题是,如果要存储 Integer,还须要为Integer 独自编写一种ArrayList

public class IntegerArrayList {private Integer[] array;
    private int size;
    public void add(Integer e) {...}
    public void remove(int index) {...}
    public Integer get(int index) {...}
}

实际上,还须要为其余所有 class 独自编写一种ArrayList

  • LongArrayList
  • DoubleArrayList
  • PersonArrayList

这是不可能的,JDK 的 class 就有上千个,而且它还不晓得其他人编写的 class。

为了解决新的问题,咱们必须把 ArrayList 变成一种模板:ArrayList<T>,代码如下:

public class ArrayList<T> {private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

T能够是任何 class。这样一来,咱们就实现了:编写一次模版,能够创立任意类型的ArrayList

// 创立能够存储 String 的 ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创立能够存储 Float 的 ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创立能够存储 Person 的 ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

因而,泛型就是定义一种模板,例如ArrayList<T>,而后在代码中为用到的类创立对应的ArrayList< 类型 >

ArrayList<String> strList = new ArrayList<String>();

由编译器针对类型作查看:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

这样一来,既实现了编写一次,万能匹配,又通过编译器保障了类型平安:这就是泛型。

向上转型

在 Java 规范库中的 ArrayList<T> 实现了 List<T> 接口,它能够向上转型为List<T>

public class ArrayList<T> implements List<T> {...}

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

即类型 ArrayList<T> 能够向上转型为List<T>

要_特地留神_:不能把 ArrayList<Integer> 向上转型为 ArrayList<Number>List<Number>

这是为什么呢?假如 ArrayList<Integer> 能够向上转型为ArrayList<Number>,察看一下代码:

// 创立 ArrayList<Integer> 类型:ArrayList<Integer> integerList = new ArrayList<Integer>();
// 增加一个 Integer:integerList.add(new Integer(123));
//“向上转型”为 ArrayList<Number>:ArrayList<Number> numberList = integerList;
// 增加一个 Float,因为 Float 也是 Number:numberList.add(new Float(12.34));
// 从 ArrayList<Integer> 获取索引为 1 的元素(即增加的 Float):Integer n = integerList.get(1); // ClassCastException!

咱们把一个 ArrayList<Integer> 转型为 ArrayList<Number> 类型后,这个 ArrayList<Number> 就能够承受 Float 类型,因为 FloatNumber的子类。然而,ArrayList<Number>实际上和 ArrayList<Integer> 是同一个对象,也就是 ArrayList<Integer> 类型,它不可能承受 Float 类型,所以在获取 Integer 的时候将产生ClassCastException

实际上,编译器为了防止这种谬误,基本就不容许把 ArrayList<Integer> 转型为ArrayList<Number>

ArrayList<Integer> 和 ArrayList<Number> 两者齐全没有继承关系。

小结

泛型就是编写模板代码来适应任意类型;

泛型的益处是应用时不用对类型进行强制转换,它通过编译器对类型进行查看;

留神泛型的继承关系:能够把 ArrayList<Integer> 向上转型为 List<Integer>T 不能变!),但不能把 ArrayList<Integer> 向上转型为 ArrayList<Number>T 不能变成父类)。

应用泛型

应用 ArrayList 时,如果不定义泛型类型时,泛型类型实际上就是Object

// 编译器正告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

此时,只能把 <T> 当作 Object 应用,没有施展泛型的劣势。

当咱们定义泛型类型 <String> 后,List<T>的泛型接口变为强类型List<String>

// 无编译器正告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);

当咱们定义泛型类型 <Number> 后,List<T>的泛型接口变为强类型List<Number>

List<Number> list = new ArrayList<Number>();
list.add(new Integer(123));
list.add(new Double(12.34));
Number first = list.get(0);
Number second = list.get(1);

编译器如果能主动推断出泛型类型,就能够省略前面的泛型类型。例如,对于上面的代码:

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

编译器看到泛型类型 List<Number> 就能够主动推断出前面的 ArrayList<T> 的泛型类型必须是ArrayList<Number>,因而,能够把代码简写为:

// 能够省略前面的 Number,编译器能够主动推断泛型类型:List<Number> list = new ArrayList<>();

泛型接口

除了 ArrayList<T> 应用了泛型,还能够在接口中应用泛型。例如,Arrays.sort(Object[])能够对任意数组进行排序,但待排序的元素必须实现 Comparable<T> 这个泛型接口:

public interface Comparable<T> {
    /**
     * 返回正数: 以后实例比参数 o 小
     * 返回 0: 以后实例与参数 o 相等
     * 返回负数: 以后实例比参数 o 大
     */
    int compareTo(T o);
}

小结

应用泛型时,把泛型参数 <T> 替换为须要的 class 类型,例如:ArrayList<String>ArrayList<Number>等;

能够省略编译器能主动推断出的类型,例如:List<String> list = new ArrayList<>();

不指定泛型参数类型时,编译器会给出正告,且只能将 <T> 视为 Object 类型;

能够在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

编写泛型

编写泛型类比一般类要简单。通常来说,泛型类个别用在汇合类中,例如ArrayList<T>,咱们很少须要编写泛型类。

如果咱们的确须要编写一个泛型类,那么,应该如何编写它?

能够依照以下步骤来编写一个泛型类。

首先,依照某种类型,例如:String,来编写类:

public class Pair {
    private String first;
    private String last;
    public Pair(String first, String last) {
        this.first = first;
        this.last = last;
    }
    public String getFirst() {return first;}
    public String getLast() {return last;}
}

最初,把特定类型 String 替换为T,并申明<T>

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {return first;}
    public T getLast() {return last;}
}

纯熟后即可间接从 T 开始编写。

静态方法

编写泛型类时,要特地留神,泛型类型 <T> 不能用于静态方法。

多个泛型类型

泛型还能够定义多种类型。例如,咱们心愿 Pair 不总是存储两个类型一样的对象,就能够应用类型<T, K>

public class Pair<T, K> {
    private T first;
    private K last;
    public Pair(T first, K last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ...}
    public K getLast() { ...}
}

应用的时候,须要指出两种类型:

Pair<String, Integer> p = new Pair<>("test", 123);

Java 规范库的 Map<K, V> 就是应用两种泛型类型的例子。它对 Key 应用一种类型,对 Value 应用另一种类型。

小结

编写泛型时,须要定义泛型类型<T>

静态方法不能引用泛型类型<T>,必须定义其余类型(例如<K>)来实现动态泛型办法;

泛型能够同时定义多种类型,例如Map<K, V>

擦拭法

Java 的泛型是由编译器在编译时履行的,编译器外部永远把所有类型 T 视为 Object 解决,然而,在须要转型的时候,编译器会依据 T 的类型主动为咱们履行平安地强制转型。

理解了 Java 泛型的实现形式——擦拭法,咱们就晓得了 Java 泛型的局限:

局限一:<T>不能是根本类型,例如 int,因为理论类型是ObjectObject 类型无奈持有根本类型:

Pair<int> p = new Pair<>(1, 2); // compile error!

局限二:无奈获得带泛型的Class

小结

Java 的泛型是采纳擦拭法实现的;

擦拭法决定了泛型<T>

  • 不能是根本类型,例如:int
  • 不能获取带泛型类型的Class,例如:Pair<String>.class
  • 不能判断带泛型类型的类型,例如:x instanceof Pair<String>
  • 不能实例化 T 类型,例如:new T()

泛型办法要避免反复定义方法,例如:public boolean equals(T obj)

子类能够获取父类的泛型类型<T>

汇合

汇合类型是 Java 规范库中被应用最多的类型。

汇合简介

什么是汇合(Collection)?汇合就是“由若干个确定的元素所形成的整体”。例如,5 只小兔形成的汇合。

在数学中,咱们常常遇到汇合的概念。例如:

  • 无限汇合:

    • 一个班所有的同学形成的汇合;
    • 一个网站所有的商品形成的汇合;
  • 有限汇合:

    • 整体自然数汇合:1,2,3,……
    • 有理数汇合;
    • 实数汇合;

为什么要在计算机中引入汇合呢?这是为了便于解决一组相似的数据,例如:

  • 计算所有同学的总成绩和均匀问题;
  • 列举所有的商品名称和价格;
  • ……

在 Java 中,如果一个 Java 对象能够在外部持有若干其余 Java 对象,并对外提供拜访接口,咱们把这种 Java 对象称为汇合。很显然,Java 的数组能够看作是一种汇合:

String[] ss = new String[10]; // 能够持有 10 个 String 对象
ss[0] = "Hello"; // 能够放入 String 对象
String first = ss[0]; // 能够获取 String 对象

既然 Java 提供了数组这种数据类型,能够充当汇合,那么,咱们为什么还须要其余汇合类?这是因为数组有如下限度:

  • 数组初始化后大小不可变;
  • 数组只能按索引程序存取。

因而,咱们须要各种不同类型的汇合类来解决不同的数据,例如:

  • 可变大小的程序链表;
  • 保障无反复元素的汇合;

Collection

Java 规范库自带的 java.util 包提供了汇合类:Collection,它是除 Map 外所有其余汇合类的根接口。Java 的 java.util 包次要提供了以下三种类型的汇合:

  • List:一种有序列表的汇合,例如,按索引排列的 StudentList
  • Set:一种保障没有反复元素的汇合,例如,所有无反复名称的 StudentSet
  • Map:一种通过键值(key-value)查找的映射表汇合,例如,依据 Studentname查找对应 StudentMap

Java 汇合的设计有几个特点:一是实现了接口和实现类相拆散,例如,有序表的接口是 List,具体的实现类有ArrayListLinkedList 等,二是反对泛型,咱们能够限度在一个汇合中只能放入同一种数据类型的元素,例如:

List<String> list = new ArrayList<>(); // 只能放入 String 类型

最初,Java 拜访汇合总是通过对立的形式——迭代器(Iterator)来实现,它最显著的益处在于无需晓得汇合外部元素是按什么形式存储的。

因为 Java 的汇合设计十分长远,两头经验过大规模改良,咱们要留神到有一小部分汇合类是遗留类,不应该持续应用:

  • Hashtable:一种线程平安的 Map 实现;
  • Vector:一种线程平安的 List 实现;
  • Stack:基于 Vector 实现的 LIFO 的栈。

还有一小部分接口是遗留接口,也不应该持续应用:

  • Enumeration<E>:已被 Iterator<E> 取代。

小结

Java 的汇合类定义在 java.util 包中,反对泛型,次要提供了 3 种汇合类,包含 ListSetMap。Java 汇合应用对立的 Iterator 遍历,尽量不要应用遗留接口。

应用 List

在汇合类中,List是最根底的一种汇合:它是一种有序列表。

List的行为和数组简直完全相同:List外部依照放入元素的先后顺序寄存,每个元素都能够通过索引确定本人的地位,List的索引和数组一样,从 0 开始。

数组和 List 相似,也是有序构造,如果咱们应用数组,在增加和删除元素的时候,会十分不不便。例如,从一个已有的数组 {'A', 'B', 'C', 'D', 'E'} 中删除索引为 2 的元素:

┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │   │
└───┴───┴───┴───┴───┴───┘
              │   │
          ┌───┘   │
          │   ┌───┘
          │   │
          ▼   ▼
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ D │ E │   │   │
└───┴───┴───┴───┴───┴───┘

这个“删除”操作实际上是把 'C' 前面的元素顺次往前挪一个地位,而“增加”操作实际上是把指定地位当前的元素都顺次向后挪一个地位,腾出来的地位给新加的元素。这两种操作,用数组实现十分麻烦。

因而,在理论利用中,须要增删元素的有序列表,咱们应用最多的是 ArrayList。实际上,ArrayList 在外部应用了数组来存储所有元素。例如,一个 ArrayList 领有 5 个元素,理论数组大小为6(即有一个空位):

size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │   │
└───┴───┴───┴───┴───┴───┘

当增加一个元素并指定索引到 ArrayList 时,ArrayList主动挪动须要挪动的元素:

size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │   │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘

而后,往外部指定索引的数组地位增加一个元素,而后把 size1

size=6
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘

持续增加元素,然而数组已满,没有闲暇地位的时候,ArrayList先创立一个更大的新数组,而后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组:

size=6
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │   │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

当初,新数组就有了空位,能够持续增加一个元素到数组开端,同时 size1

size=7
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │ G │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

可见,ArrayList把增加和删除的操作封装起来,让咱们操作 List 相似于操作数组,却不必关怀外部元素如何挪动。

咱们考查 List<E> 接口,能够看到几个次要的接口办法:

  • 在开端增加一个元素:boolean add(E e)
  • 在指定索引增加一个元素:boolean add(int index, E e)
  • 删除指定索引的元素:int remove(int index)
  • 删除某个元素:int remove(Object e)
  • 获取指定索引的元素:E get(int index)
  • 获取链表大小(蕴含元素的个数):int size()

然而,实现 List 接口并非只能通过数组(即 ArrayList 的实现形式)来实现,另一种 LinkedList 通过“链表”也实现了 List 接口。在 LinkedList 中,它的外部每个元素都指向下一个元素:

        ┌───┬───┐   ┌───┬───┐   ┌───┬───┐   ┌───┬───┐
HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │   │
        └───┴───┘   └───┴───┘   └───┴───┘   └───┴───┘

通常状况下,咱们总是优先应用ArrayList

正文完
 0