异常
Error 和 Exception
Exception 和 Error ,二者都是 Java 异常处理的重要子类,各自都包含大量子类,均继承自Throwable 类。
Error 表示系统级的错误,是java 运行环境内部错误或者硬件问题,不能指望程序来处理这样的问题,除了退出运行外别无选择,它是Java 虚拟机抛出的。
Exception 表示程序需要捕捉、需要处理的异常,是由与程序设计的不完善而出现的问题,程序必须处理的问题。
异常类型
Java中的异常,主要可以分为两大类:即受检异常(checked exception)和 非受检异常(unchecked exception)
受检异常
对于受检异常来说,如果一个方法在声明的过程中证明了其要有受检异常抛出:
public void test() throws xxxExcetion{ }
那么,当我们在程序中调用它的时候,一定要对该异常进行处理(捕获或者向上抛出),否则是无法编译通过的,这是一种强制规范。
这种异常在IO操作中比较多。比如FileNotFoundException,当我们使用IO流处理一个文件的时候,有一种特殊情况,就是文件不存在,所以,在文件处理的接口定义时他会显式抛出FileNotFoundException,目的就是告诉这个方法的调用者,我这个⽅法不保证⼀定可以成功, 是有可能找不到对应的⽂件 的, 你要明确的对这种情况做特殊处理哦。
所以说, 当我们希望我们的⽅法调⽤者, 明确的处理⼀些特殊情况的时候, 就应该使⽤受检异常。
非受检异常
对于非受检异常来说,一般是运行时异常,继承自RuntimeException。在编写代码的时候,不需要显式的捕获,但是如果不捕获,在运行期如果发生异常就会中断程序的执行。
这种异常一般可以理解为是代码原因导致的。比如发生空指针、数组越界等。所以,只要代码写的没问题, 这些异常都是可以避免的。 也就不需要我们显⽰的进⾏处理。
试想⼀下, 如果你要对所有可能发⽣空指针的地⽅做异常处理的话, 那相当于你的所有代码都需要做这件事。
异常相关的关键字
throws
、throw
、try
、catch
、finally
try
:用来指定⼀块预防所有异常的程序;
catch
:子句紧跟在try块后面,用来指定你想要捕获的异常的类型;
finally
:为确保⼀段代码不管发⽣什么异常状况都要被执⾏;
throw
:语句⽤来明确地抛出⼀个异常;(通常是对象)
throws
:⽤来声明⼀个⽅法可能抛出的各种异常;(通常是类型)
正确处理异常
异常的处理方式有两种:1. 自己处理。 2.向上抛出,交给调用者处理。
异常,千万不能捕获了之后什么也不做。 或者只是使⽤e.printStacktrace
。
具体的处理⽅式的选择其实原则⽐较简明: ⾃⼰明确的知道如何处理的, 就要处理掉。 不知道如何处理的, 就交给调⽤者处理。
自定义异常
自定义异常就是开发人员自己定义的异常,一般通过继承Exception
的子类的方式实现。
编写⾃定义异常类实际上是继承⼀个API标准异常类, ⽤新定义的异常处理信息覆盖原有信息的过程。
这种⽤法在Web开发中也⽐较常见, ⼀般可以⽤来⾃定义业务异常。 如余额不⾜、 重复提交等。 这种⾃定义异常有业务含义, 更容易让上层理解和处理。
异常链
“异常链”是Java中非常流行的异常处理的概念。是指在进行一个异常处理时抛出了另外一个异常,由此产生了一个异常链条。
该技术大多用于将“ 受检查异常” ( checked exception) 封装成为“⾮受检查异常”( unchecked exception)或者RuntimeException。
顺便说⼀下, 如果因为因为异常你决定抛出⼀个新的异常, 你⼀定要包含原有的异常, 这样, 处理程序才可以通过getCause()和initCause()⽅法来访问异常最终的根源。
从 Java 1.4版本开始,几乎所有的异常都支持异常链。
以下是Throwable中支持异常链的方法和构造函数。
Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)
initCause 和 Throwable 构造函数的Throwable参数是导致当前异常的异常。getCause返回导致当前异常的异常,initCause设置当前异常的原因。
以下示例显示如何使用异常链。
try {
} catch (IOException e) {
throw new SampleException("Other IOException", e);
}
在此示例中,当捕获到IOException时,将创建一个新的SampleException异常,并附加原始的异常原因,并将异常链抛出到下一个更高级别的异常处理程序。
try-with-resources
Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。
关闭资源的常用方式就是在finally块里是释放,即调用close方法。比如,我们经常会写这样的代码:
public static void main(String[] args) {
BufferedReader br = null;
try {
String line;
br = new BufferedReader(new FileReader("d:\\hollischuang.xml"));
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException ex) {
// handle exception
}
}
}
从Java7开始,jdk提供了一种更好的方式关闭资源,使用try-with-resources语句,改写一下上面的代码,效果如下:
public static void main(String... args) {
try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
}
}
看,这简直是一大福音啊,虽然我之前一般使用IOUtils去关闭流,并不会使用在finally中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。看下他的背后:
public static transient void main(String args[])
{
BufferedReader br;
Throwable throwable;
br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"));
throwable = null;
String line;
try
{
while((line = br.readLine()) != null)
System.out.println(line);
}
catch(Throwable throwable2)
{
throwable = throwable2;
throw throwable2;
}
if(br != null)
if(throwable != null)
try
{
br.close();
}
catch(Throwable throwable1)
{
throwable.addSuppressed(throwable1);
}
else
br.close();
break MISSING_BLOCK_LABEL_113;
Exception exception;
exception;
if(br != null)
if(throwable != null)
try
{
br.close();
}
catch(Throwable throwable3)
{
throwable.addSuppressed(throwable3);
}
else
br.close();
throw exception;
IOException ioexception;
ioexception;
}
}
其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。
finally 和 return 的执行顺序
try()
里面有一个return
语句, 那么后⾯的finally{}
⾥⾯的code会不会被执⾏, 什么时候执⾏, 是在return
前还是return
后?
如果try中有return语句, 那么finally中的代码还是会执⾏。因为return表⽰的是要整个⽅法体返回, 所以,finally中的语句会在return之前执⾏。
但是return前执行的finally块内,对数据的修改效果对于引用类型和值类型会不同
// 测试 修改值类型
static int f() {
int ret = 0;
try {
return ret; // 返回 0,finally内的修改效果不起作用
} finally {
ret++;
System.out.println("finally执行");
}
}
// 测试 修改引用类型
static int[] f2(){
int[] ret = new int[]{0};
try {
return ret; // 返回 [1],finally内的修改效果起了作用
} finally {
ret[0]++;
System.out.println("finally执行");
}
}
注解
元注解
说简单点,就是定义其他注解的注解,比如Override这个注解,就不是一个元注解。而是通过元注解定义出来的。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这里面的@Target @Retention 就是元注解。
元注解有四个:
@Target
:表示该注解可以用到什么地方@Retention
:表示在什么级别保存该注解信息。Documented
:将此注解包含在javadoc中Inherited
:允许子类继承父类的注解。
自定义注解
除了元注解,都是自定义注解。通过元注解定义出来的注解。 如我们常用的Override 、Autowire等。 日常开发中也可以自定义一个注解,这些都是自定义注解。
Java中常见注解使用
@Override
:表示当前方法覆盖了父类的方法@Deprecation
表示方法已经过时,方法上有横线,使用时会有警告。@SuppressWarnings
表示关闭一些警告信息(通知java编译器忽略特定的编译警告)SafeVarargs
(jdk1.7更新) 表示:专门为抑制“堆污染”警告提供的。@FunctionalInterface
(jdk1.8更新) 表示:用来指定某个接口必须是函数式接口,否则就会编译出错。
Spring常用注解
@Configuration
把一个类作为一个IoC容器,它的某个方法头上如果注册了@Bean
,就会作为这个Spring容器中的Bean。@Scope
注解 作用域@Service
用于标注业务层组件@Controller
用于标注控制层组件@Repository
用于标注数据访问组件,即DAO组件。@Component
泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。@Scope
用于指定scope作用域的(用在类上)@PostConstruct
用于指定初始化方法(用在方法上)@PreDestory
用于指定销毁方法(用在方法上)@DependsOn
:定义Bean初始化及销毁时的顺序@Primary
:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常@Autowired
默认按类型装配,如果我们想使用按名称装配,可以结合@Qualifier
注解一起使用。如下:@Autowired @Qualifier("personDaoBean")
存在多个实例配合使用@Resource
默认按名称装配,当找不到与名称匹配的bean才会按类型装配。@PostConstruct
初始化注解@PreDestroy
摧毁注解 默认 单例 启动就加载
Spring中的这几个注解有什么区别:@Component、@Repository、@Service、@Controller
-
@Component指的是组件,
@Controller,@Repository和@Service 注解都被@Component修饰,用于代码中区分表现层,持久层和业务层的组件,代码中组件不好归类时可以使用@Component来标注
-
当前版本只有区分的作用,未来版本可能会添加更丰富的功能
注解和反射的结合
注解和反射经常结合在一起使用,在很多框架的代码中都能看到他们结合使用的影子
可以通过反射来判断类、方法、字段上是否有某个注解以及获取注解中的值,获取某个类中方法上的注解代码示例如下:
Class<?> clz = bean.getClass();
Method[] methods = clz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(EnableAuth.class)) {
String name = method.getAnnotation(EnableAuth.class).name();
}
}
通过isAnnotationPresent判断是否存在某个注解,通过getAnnotation获取注解对象,然后获取值。
示例
示例参考:https://blog.csdn.net/KKALL1314/article/details/96481557
自己写了一个例子,实现功能如下:
一个类的某些字段上被注解标识,在读取该属性时,将注解中的默认值赋给这些属性,没有标记的属性不赋值
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface MyAnno {
String value() default "有注解";
}
定义一个类
@Data
@ToString
public class Person {
@MyAnno
private String stra;
private String strb;
private String strc;
public Person(String str1,String str2,String str3){
super();
this.stra = str1;
this.strb = str2;
this.strc = str3;
}
}
这里给str1加了注解,并利用反射解析并赋值:
public class MyTest {
public static void main(String[] args) {
//初始化全都赋值无注解
Person person = new Person("无注解","无注解","无注解");
//解析注解
doAnnoTest(person);
System.out.println(person.toString());
}
private static void doAnnoTest(Object obj) {
Class clazz = obj.getClass();
Field[] declareFields = clazz.getDeclaredFields();
for (Field field:declareFields) {
//检查该类是否使用了某个注解
if(clazz.isAnnotationPresent(MyAnno.class)){
MyAnno anno = field.getAnnotation(MyAnno.class);
if(anno!=null){
String fieldName = field.getName();
try {
Method setMethod = clazz.getDeclaredMethod("set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1),String.class);
//获取注解的属性
String annoValue = anno.value();
//将注解的属性值赋给对应的属性
setMethod.invoke(obj,annoValue);
}catch (NoSuchMethodException e){
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
}
运行结果:
Person(stra=有注解, strb=无注解, strc=无注解)
当开发者使用了Annotation 修饰了类、方法、Field 等成员之后,这些 Annotation 不会自己生效,必须由开发者提供相应的代码来提取并处理 Annotation 信息。这些处理提取和处理 Annotation 的代码统称为 APT(Annotation Processing Tool)。
注解的提取需要借助于 Java 的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。
如何定义一个注解
在Java中,类使用class定义,接口使用interface定义,注解和接口的定义差不多,增加了一个@符号,即@interface,代码如下:
public @interface EnableAuth {
}
注解中可以定义成员变量,用于信息的描述,跟接口中方法的定义类似,代码如下:
public @interface EnableAuth {
String name();
}
还可以添加默认值:
public @interface EnableAuth {
String name() default "猿天地";
}
上面的介绍只是完成了自定义注解的第一步,开发中日常使用注解大部分是用在类上,方法上,字段上,示列代码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableAuth {
}
Target
用于指定被修饰的注解修饰哪些程序单元,也就是上面说的类,方法,字段
Retention
用于指定被修饰的注解被保留多长时间,分别SOURCE(注解仅存在于源码中,在class字节码文件中不包含,即编译后消失),CLASS(默认的保留策略,注解会在class字节码文件中存在,但运行时无法获取),RUNTIME(注解会在class字节码文件中存在,在运行时可以通过反射获取到)三种类型,如果想要在程序运行过程中通过反射来获取注解的信息需要将Retention设置为RUNTIME
Documented
用于指定被修饰的注解类将被javadoc工具提取成文档
Inherited
用于指定被修饰的注解类将具有继承性
泛型
什么是泛型
Java泛型(generics) 是JDK5 中引入的一个新特性, 允许在定义类和接⼜的时候使⽤类型参数( type parameter) 。
声明的类型参数在使⽤时⽤具体的类型来替换。 泛型最主要的应⽤是在JDK 5中的新集合类框架中。
泛型最⼤的好处是可以提⾼代码的复⽤性。 以List接⼜为例,我们可以将String、 Integer等类型放⼊List中, 如不⽤泛型, 存放String类型要写⼀个List接口, 存放Integer要写另外⼀个List接口, 泛型可以很好的解决这个问题。
类型擦除
什么是类型擦除
类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。 类型擦除的主要过程如下: 1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。2.移除所有的类型参数。
Java 编译器处理泛型的过程
code1:
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("name", "hollis");
map.put("age", "22");
System.out.println(map.get("name"));
System.out.println(map.get("age"));
}
反编译后的code 1:
public static void main(String[] args) {
Map map = new HashMap();
map.put("name", "hollis");
map.put("age", "22");
System.out.println((String) map.get("name"));
System.out.println((String) map.get("age"));
}
我们发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型,
code2:
interface Comparable<A> {
public int compareTo(A that);
}
public final class NumericValue implements Comparable<NumericValue> {
private byte value;
public NumericValue(byte value) {
this.value = value;
}
public byte getValue() {
return value;
}
public int compareTo(NumericValue that) {
return this.value - that.value;
}
}
反编译后的code 2:
interface Comparable {
public int compareTo( Object that);
}
public final class NumericValue
implements Comparable
{
public NumericValue(byte value)
{
this.value = value;
}
public byte getValue()
{
return value;
}
public int compareTo(NumericValue that)
{
return value - that.value;
}
public volatile int compareTo(Object obj)
{
return compareTo((NumericValue)obj);
}
private byte value;
}
code3:
public class Collections {
public static <A extends Comparable<A>> A max(Collection<A> xs) {
Iterator<A> xi = xs.iterator();
A w = xi.next();
while (xi.hasNext()) {
A x = xi.next();
if (w.compareTo(x) < 0)
w = x;
}
return w;
}
}
反编译后的code 3:
public class Collections
{
public Collections()
{
}
public static Comparable max(Collection xs)
{
Iterator xi = xs.iterator();
Comparable w = (Comparable)xi.next();
while(xi.hasNext())
{
Comparable x = (Comparable)xi.next();
if(w.compareTo(x) < 0)
w = x;
}
return w;
}
}
第2个泛型类Comparable<A>
擦除后A被替换为最左边界Object
,Comparable<NumbericVavlue>
的类型参数NumbericValue
被擦除掉,但这直接导致NumbericValue
没有实现接口Comparable的compareTo(Object object)
方法,于是编译器充当好人,添加了一个桥接方法。第3个示例中限定了类型参数的边界<A extends Comparable<A>
,A必须为Comparable<A>
的子类,按照类型擦除的过程,先讲所有的类型参数替换为最左边界Comparable<A>
,然后去掉参数类型A,得到最终的擦除后结果。
泛型带来的问题
一、泛型遇到重载
**public class GenericTypes {
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}
上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List<String>
另一个是List ,但是,这段代码是编译通不过的。因为我们前面讲过,参数List<Integer>
和List<String>
编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
二、当泛型遇到catch
如果我们自定义了一个泛型异常类GenericException,那么,不要尝试用多个catch取匹配不同的异常类型,例如你想要分别捕获GenericException、GenericException,这也是有问题的。
三、当泛型遇到静态变量
public class StaticTest{
public static void main(String[] args){
GT<Integer> gti = new GT<Integer>();
gti.var=1;
GT<String> gts = new GT<String>();
gts.var=2;
System.out.println(gti.var);
}
}
class GT<T>{
public static int var=0;
public void nothing(T x){}
}
答案是 2
由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。
泛型中 K T V E ? object 等的含义
E–> Element(在集合中使用,因为集合中存放的是元素)
T --> Type (Java 类)
K --> Key (键)
V—> Value (值)
N—> Number(数值类型)
? - 表示不确定的java类型(无限制通配符类型)
Object - 是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。
限定通配符和非限定通配符
限定通配符
对类型进⾏限制, 泛型中有两种限定通配符:
- 表示类型的上界:格式为
<? extends T>
,即类型必须是T类型或者T子类 - 表示类型的下界:格式为
<? super T>
,即类型必须为T类型或T的父类
泛型类型必须⽤限定内的类型来进⾏初始化,否则会导致编译错误。
⾮限定通配符
表⽰可以⽤任意泛型类型来替代,类型为
先看一个例子:
public class Food {}
public class Fruit extends Food {}
public class Apple extends Fruit {}
public class Banana extends Fruit{}
public class GenericTest {
public void testExtends(List<? extends Fruit> list){
//报错,extends为上界通配符,只能取值,不能放.
//因为Fruit的子类不只有Apple还有Banana,这里不能确定具体的泛型到底是Apple还是Banana,所以放入任何一种类型都会报错
//list.add(new Apple());
//可以正常获取
Fruit fruit = list.get(1);
}
public void testSuper(List<? super Fruit> list){
//super为下界通配符,可以存放元素,但是也只能存放当前类或者子类的实例,以当前的例子来讲,
//无法确定Fruit的父类是否只有Food一个(Object是超级父类)
//因此放入Food的实例编译不通过
list.add(new Apple());
// list.add(new Food());
Object object = list.get(1);
}
}
在testExtends方法中,因为泛型中用的是extends,在向list中存放元素的时候,我们并不能确定List中的元素的具体类型,即可能是Apple也可能是Banana。因此调用add方法时,不论传入new Apple()还是new Banana(),都会出现编译错误。
理解了extends之后,再看super就很容易理解了,即我们不能确定testSuper方法的参数中的泛型是Fruit的哪个父类,因此在调用get方法时只能返回Object类型。结合extends可见,在获取泛型元素时,使用extends获取到的是泛型中的上边界的类型(本例子中为Fruit),范围更小。
在使用泛型时,存取元素时用super,获取元素时,用extends。
频繁往外读取内容的,适合用上界Extends。经常往里插入的,适合用下界Super。
List和原始类型List之间的区别?
原始类型List 和带参数类型List<Object>
直接的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。
通过使用Object 作为类型,可以告知编译器该方法可以接受任何类型的对象。比如String或Integer。
它们之间的第二点区别是,你可以把任何带参数的类型传递给原始类型List,但却不能把List传递给接受 List的方法,因为会产生编译错误。
List 和 List 之间的区别是什么?
List<?>
是一个未知类型的List,而List<Object>
其实是任意类型的List。你可以把List<String>
, List<Integer>
赋值给List<?>
,却不能把List<String>
赋值给 List<Object>
。