【第20、21天】Java的内部类、异常处理机制

版权声明:©2018 本文為博主來秋原創,轉載請註明出處,博主保留追責權利。 https://blog.csdn.net/qq_30257081/article/details/84639960

1 内部类

       在一个类的类体中定义的类。

  • 为什么要使用内部类?

    内部类是Java当中共享数据最最简单的方式之一。

  • 内部类都是类,编译后都会产生.class文件。其中成员内部类、静态内部类、局部内部类的命名规则为 “外部类名$内部类名.class”;匿名内部类的命名为 “外部类名$第几个被加载匿名类的序号.class”。在操作系统中文件系统的文件命名“.”意为命名和扩展名的分隔符,所以换用“$”。

  • Java当中共享数据的三种方式 ★

    • 静态变量共享
    • 参数传递共享 (构造方法、setter(着重使用,更灵活))
    • 内部类共享

例题:使用三种方式实现“Bobby和Cindy此时共用一个MP3听歌单中第9首歌”

//1.使用静态变量共享数据

public class ExampleStatic{
	public static void main(String[] args){
		Bobby bobby = new Bobby();
		Cindy cindy = new Cindy();
		bobby.listen();
		cindy.listen();
	}
}

class MP3{
	//现在在听歌单的第9首歌,使多个来调用的对象共享一份变量值
	public static int serialNum = 9;
}

class Bobby{
	public void listen(){
		System.out.println("Bobby现在在听第" + MP3.serialNum + "首歌");
	}
}

class Cindy{
	public void listen(){
		System.out.println("Cindy现在在听第" + MP3.serialNum + "首歌");
	}
}
//2.使用参数传递共享数据

public class ExampleArgs{
	public static void main(String[] args){
	int serialNum = 9;
	Bobby bobby = new Bobby();
	bobby.setSerialNum(serialNum);
	Cindy cindy = new Cindy();
	cindy.setSerialNum(serialNum);
	bobby.listen();
	cindy.listen();
	}
}

class Bobby{
	private int serialNum;
	public void setSerialNum(int serialNum){
		this.serialNum = serialNum;
	}
	public void listen(){
		System.out.println("Bobby现在在听第" + serialNum + "首歌");
	}
}

class Cindy{
	private int serialNum;
	public void setSerialNum(int serialNum){
		this.serialNum = serialNum;
	}
	public void listen(){
		System.out.println("Cindy现在在听第" + serialNum + "首歌");
	}
}
//3.使用内部类共享数据

public class ExampleInnerClass{
	public static void main(String[] args){
		//定义一个外部类对象,里面内部类同步调用
		MP3 mp3 = new MP3();
		MP3.Bobby bobby = mp3.new Bobby();
		MP3.Cindy cindy = mp3.new Cindy();
		bobby.listen();
		cindy.listen();
	}
}

class MP3{
	static int serialNum = 9;
	class Bobby{
		public void listen(){
			System.out.println("Bobby现在在听第" + serialNum + "首歌");
		}
	}

	class Cindy{
		public void listen(){
			System.out.println("Cindy现在在听第" + serialNum + "首歌");
		}
	}
}
  • 内部类的分类及能共享到什么数据
名称 定义位置 能共享到的数据
成员内部类 类体 外部类的所有(静态+非静态)成员(属性+方法)。
静态内部类 类体,但在class前面加static修饰 外部类的静态成员(属性+方法)。
局部内部类 成员方法内 对于访问外部类的成员,取决于外部的方法是否为静态变量:是,只能访问外部类静态成员;不是,访问外部类所有成员。能访问外部方法中修饰为final的变量(JDK7.0后底层默认添加final属性,若修改就编译报错)。
匿名内部类 自由 因能定义的位置比较自由,定义时符合以上三种哪一个,共享到的数据就与哪个相符。

1.1 成员内部类

  • 定义方式:
class Outer{
	//Outer类成员
	class Inner{
		//Inner类成员
	}
}
  • 创建对象:
    Outer.Inner in = new Outer().new Inner();

    两个内部类共享一个外部类的方式:
    Outer out = new Outer();
    Outer.Inner in1 = out.new Inner();
    Outer.Inner in2 = out.new Inner();

  • 与外部类的关系: 蛔虫与动物。

  • 为什么成员内部类方法中能共享到外部类的成员?
    思路:若成员内部类的方法加载了,成员内部类对象便加载了,外部类对象也一定加载了。既然外部类对象加载了,外部成员也就加载了,所以可以访问。

  • 成员内部类中不能定义static成员。

  • 判断声明对象数量的方法: 数new,new了几次就声明了几个。

1.2 静态内部类

  • 定义方式:
class Outer{
	//Outer类成员
	static class Inner{
		//Inner类成员
	}
}
  • 创建对象:
    Outer.Inner in = new Outer.Inner();

  • 与外部类的关系: 寄居蟹和蛤蜊壳。

  • 为什么静态成员内部类方法中只能共享到外部类的静态成员?
    静态内部类的加载不需依附外部类(静态内部类和外部类没有依赖关系)。
    静态内部类在第一次被使用时(注意并不是实例化对象,不需要new也行)才加载。不过在加载静态内部类的过程中外部类也会加载,外部类加载时,外部的静态成员也会加载,而非静态成员只有在使用new实例化对象时才会被加载,所以无法访问。

1.3 局部内部类

  • 定义方式:
class Outer{
	//Outer类成员
		
	public void functionA(){
		//functionA的局部变量
		//局部内部类
		class Inner{			
			//Inner类成员
			public void test(){
			//test的局部变量
			}
		}
	}
		//有位置限定:定义完成之后 所在方法结束之前
		Inner in = new Inner();
		in.test();
}
  • 创建对象:

    • 有位置限定:类定义完成之后,外部方法执行(大括号)结束之前。
      Inner in = new Inner();
  • 如何理解其与外部类的关系:
    老师与学生的关系,老师只教学生一段时间(这里的一段时间可类比为外部方法生存的时间)。

1.4 匿名内部类

  • 什么场景下内部类需要被匿名?

    • 定义某个内部类只为了使用完成某个功能,名字不具有意义。

    • 在某些场景下,这个内部类的父类或实现的接口比这个类类名更重要。

  • 使用方法(结合TreeSet的比较器)

//原来的比较器定义与使用(将TreeSet中数据降序排列)
import java.util.*;
public class TestFormer{
	public static void main(String[] args){
		Set<Integer> set = new TreeSet<>(new MyComparator());
		Collections.addAll(set,58, 114, 225, 310, 545, 805);
		System.out.println(set);
	}
}

class MyComparator implements Comparator<Integer>{
	@Override
	public int compare(Integer i1,Integer i2){
		return i2 - i1;
	}
}

↓↓↓↓↓↓(这个是方法体内的内部类,作用和共享到的数据与局部内部类一样)

//在匿名类中定义比较器(将TreeSet中数据降序排列)
import java.util.*;
public class TestAnonyInner{
	public static void main(String[] args){
		//虽然接口不能被new,但是在定义匿名类时可以通过new来实现并在其类体实现方法
		Set<Integer> set = new TreeSet<>(new Comparator<Integer>{
			@Override
			public int compare(Integer i1,Integer i2){
				return i2 - i1;
			}
		});
		Collections.addAll(set, 58, 114, 225, 310, 545, 805);
		System.out.println(set);
	}
}

JDK8及之后可以使用lambda表达式,可以更加简化匿名内部类,从此匿名内部类将逐渐被lambda表达式代替。
↓↓↓↓↓↓

//lambda表达式定义比较器(将TreeSet中数据降序排列)
import java.util.*;
public class TestLambda{
	public static void main(String[] args){
		//(x,y)代表compare()传入的参数,y-x代表返回值
		Set<Integer> set = new TreeSet<>((x, y) -> y - x);
		Collections.addAll(set, 58, 114, 225, 310, 545, 805);
		System.out.println(set);
	}
}
  • 另外两种定义匿名类的方式
class A{
	//这个是成员变量的内部类,作用和共享到的数据与成员内部类一样
	Comparator<Integer> MyComparator1 = new Comparator<Integer>(){
		@Override
		public int compare(Integer i1,Integer i2){
			return i2 - i1;
		}
	};
	//这个是静态成员变量的内部类,作用和共享到的数据与静态成员内部类一样
	static Comparator<Integer> MyComparator2 = new Comparator<Integer>(){
		@Override
		public int compare(Integer i1,Integer i2){
			return i2 - i1;
		}
	};
}
  • 如何继承一个父类时并顺便在其构造方法参数列表中传入值
public class TestAnonyInnerExtends{
	public static void main(String[] args){
		Restaurant kfc = new Restaurant();
		kfc.service(new Person("张三"){
			@Override
			public void eat(){
				System.out.println("吃鳕鱼堡");
			}
		});
	}
}

abstract class Person{
	String name;
	public Person(String name){
		this.name = name;
	}
	public abstract void eat();
}
class Restaurant{
	public void service(Person p){
		System.out.println("为" + p.name + "提供良好的就餐环境和就餐服务~");
		p.eat();
	}
}

2 异常处理机制

       首先明白:异常不是报错,是在编译之后的运行过程中出现的一种“例外”。

2.1 异常类的体系结构及各部分介绍

  • Java中所有可能出现的“问题”的体系结构(异常类的关系)

Java中所有可能出现的“问题”的体系结构

  • Error和Exception的区别

    • Exception只是指运行过程当中出现的例外情况。
    • Error通常指硬件或操作系统的客观原因导致的错误,程序员通过编码无法解决的问题,发生于虚拟机自身,问题相对较严重;
import java.util.*;
public class TestError{
	public static void main(String[] args){
		List<Integer> list = new ArrayList<>(2147483647);
	}
}
  • 运行时异常(RuntimeException)和非运行时异常的区别?

    • 运行时异常在编译时不需要给出异常的处理方案(不需要必须在方法参数列表后throws出这个异常,默认都已经添加好了),编译直接能通过。异常会在运行出现“例外”时直接体现出来。每个方法都需要执行运行时异常。
      这类异常继承RuntimeException类。一般是由程序逻辑错误引起的,程序员需要做的是应该尽可能从逻辑角度尽可能避免这类异常的发生,避免不了的也要想办法捕获之。
      • 常见的运行时异常
import java.util.*;
public class Example{
	public static void main(String[] args){
		//ConcurrentModificationException 并发修改异常(在迭代器循环中直接使用集合对象添加/删除)
		ArrayList<Integer> a2 = new ArrayList<>();
		a2.add(1);
		a2.add(2);
		for(int i : a2){
			if(i == 1) a2.add(5);
		}
		
		//IllegalStateException 非法状态异常(迭代器未能指到指定位置进行删除) ★
		List<Integer> list2 = new ArrayList<>(3);
		Collections.addAll(list2,11,22,33);
		Iterator<Integer> car = list2.iterator();
		car.remove();
		
		//IndexOutOfBoundsException 索引值超出边界异常(集合)
		ArrayList<Integer> a1 = new ArrayList<>(10);
		a1.get(10);
		
		//IllegalArgumentException 非法参数异常 ★
		ArrayList<Integer> list = new ArrayList<>(-5);
		
		//ClassCastException 类型造型异常(如果把c定义为Cat类,强转时会直接报错)
		//强转、instanceof时编译器要考虑这两个类是否在继承树上的一个分支上(有继承关系),否则编译报错
		//当然在一个分支上也不见得不会出异常
		Object c = new Cat();
		Student s2 = (Student) c;
		
		//StringIndexOutOfBoundsException 字符串索引值超出边界异常
		String s1 = "1235";
		System.out.println(s1.charAt(-1));
		System.out.println(s1.substring(7));
		
		//NumberFormatException 数字格式异常
		int i = Integer.parseInt("123s");
		
		//NullPointerException 空指针异常
		String s = null;
		System.out.println(s.length());
		
		//ArrayIndexOutOfBoundsException 数组索引值超出边界异常
		int[] array1 = new int[]{55, 44, 20, 30};
		System.out.println(array1[4]);
		
		//NegativeArraySizeException 负数数组大小异常
		int[] array = new int[-5];
		
		//ArithmeticException 算术异常
		System.out.println(5 / 0);
	}
}
class Student{
}

class Cat{
}
  • 非运行时异常在编译的时候就必须要明确的给出处理方案(必须在方法参数列表后面throws这个异常),否则编译无法通过。
    这类异常直接继承Exception。面对这种异常,也要尽可能写catch去捕获可能出现的异常

    • 常见的非运行时异常
      IOException(IO流)
      AWTException(抽象窗口工具)
      CloneNotSupportedException(使用clone()不实现接口)

注意:不管运行还是非运行时的异常都是运行时出现的 ,非运行时异常并不是在编译时出现的!!!!,非运行时异常在编译时无法通过只是提醒你要加这个异常的throws(就好比一定要问明白,如果真出错要咋办,并不是真出异常了)。

  • 需要注意的是前面说的子类重写(覆盖)父类方法时,子类抛出异常必须比父类要少,这里的异常指的是非运行时异常。因为运行时异常已经在方法参数后面默认添加了,在这里再谈论就没什么意义了。

2.2 异常的处理

       程序并不一定每次都会出异常,但为了程序的健壮性,程序员需要尽量周到全面地想到程序可能会出现的各种结果。

  • 为什么要处理异常?

    • 非运行时异常如果不做处理,连编译都无法通过;
    • 一旦程序运行当中出现异常,之后的代码就都不执行了。
  • 如何处理异常?

    • 抛还上级(throws),可以使非运行时异常的编译通过,但无法解决若真出现异常后代码不执行的问题。只能将问题不断跑给上级处理(只推卸责任不解决问题)。如果最后都没有try catch finally,只能传给JVM处理。如果此时出现try catch finally,程序需要执行完finally再结束。总之throws之后,这个方法都会结束。
      这种办法只能解决掉非运行时异常的编译报错问题;对于运行时异常,其异常每个类都自动throws。
    • 自行处理(try catch finally),可以使编译通过,也可以使出现异常后的代码继续执行。(当异常发生时,不应立即处理(捕获),而是应该考虑当前作用域是否有有能力处理这一异常的能力,如果没有,则应将该异常继续向上抛出,交由更上层的作用域来处理。)

出现运行时异常后,若没有捕获处理这个异常(即没有catch),系统会把异常一直往上层抛,一直到最上层,如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。运行时异常(RuntimeException)是Exception的子类,也有一般异常的特点,是可以被catch块处理的。只不过往往我们不对他处理罢了。也就是说,你如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止。
如果不想终止,则必须捕获所有的运行时异常,决不让这个处理线程退出。队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志。不应该由于异常数据而影响下面对正常数据的处理。

2.2.1 throw

       在程序的开发中,如果预计到会有“例外的情况”,我们需要自己主动创造异常出现的场景,此时就需要在方法体内主动抛出(throw)异常,使程序更加健壮。在throw时,可以使用直接加new出来的对象,也可以加引用。另外,当官方提供的异常类不足以描述我们需求的异常时,我们需要定义自己的异常类。当异常抛出,程序立即终止,后面的语句执行不到。

  • 如何抛出一个自定义异常
public class TestThrow{
	//没有明确继承运行时异常,所以需要throws这个自定义异常
	public static void main(String[] args)throws Exception{
		System.out.println(getLuckyNumber());
	}
	public static int getLuckyNumber(){
		int lucky = (int)(Math.random()*10);//0-9
		//认为2和4是不吉利的数字
		if(lucky == 2 || lucky == 4){
			//加new对象抛出
			throw new UnluckyException();
			//使用引用抛出
			/*
			UnluckyException ue = new UnluckyException();
			throw ue;
			*/
		}
		return lucky;
	}
}
//需继承一个Throwable的类,若继承RuntimeException,则这个异常就是一个运行时异常
class UnluckyException extends Exception{
	public UnluckyException(){
		//将异常信息写死在构造方法中,当然也可以定义有参构造方法,用户自定义异常信息
		super("不吉利的数字被生成了");
	}
}

抛出异常时,如果new或者引用的参数中传入字符串,这个字符串可以在catch中通过getMessage()获得。

  • throw与throws相比有什么区别?

    • throw用在方法体中,用于在没有异常出现的场景下主动的制造异常出现的场景;
    • throws用在方法签名最后,用于表达本方法当中出现指定种类的异常,本方法不做处理,抛还给调用的上级方法处理。

2.2.2 try catch finally

       当异常发生时,我们需要抛还给上级,上级如果能解决,就会使用try catch finally来搞定之。

try{
	//可能出现异常的语句;
}catch(要捕获的异常类型 异常的变量名){
	//审
}finally{
	//无论是否出现异常最终都要执行的操作
	//通常是释放和关闭资源的操作
}
  • catch中要审什么?

    • 隐瞒不报,catch中什么都不写,遇到异常什么都不报
    • 简要地审 ,打印输出e.getMessage()告诉客户简要的异常信息
    • 详细地审,e.printStackTrace() 第几行报错的轨迹(找错要从上往下找到是第一行自己写的那一行)
  • 在块中定义的变量,在块外面就消亡无法调用,这三个块的域都是独立的。如果try当中的变量在块外还要继续使用的话,那么不能将变量定义在try{}当中,否则会伴随着执行离开大括号而消亡。这时需要将定义拿到try块外面,并且赋予默认值,然后在try当中进行重新赋值。

  • try当中应当只包含一个可能出现异常的语句,这段语句完成一个操作。其中一个语句出了异常,后面的操作都会跳过。所以一个try中最好存放一句与这个异常有关的语句,实在不能放一句,那最多只能放完成一个操作的一组语句,有一句抛出异常,这组操作就不能进行。

  • 一个try,后面可以写多个不同的catch。catch中的异常类中包含异常范围必须由小到大,可以并列,但绝对不能包含。(从JDK7.0开始,一个catch的参数列表内可以写多种不同异常类型,用于进行统一的处理。但中间要用“ | ”隔开。

  • finally当中永远不应该出现return语句,如果有这个语句,try catch中的return都不会执行,都会被finally抢先返回。不管try语句是否有执行中断,在try/catch语句执行完之后,finally都将执行。

  • 如何不让finally当中的语句执行? 结束虚拟机:System.exit(0)。

2.3 异常的一些特殊情况

  • 当类体当中的某个静态属性是通过调用一个有异常声明的方法完成赋值的时候,我们根本不可能在类上直接写throws,也不可能在类体里直接try catch。此时,必须借助静态初始化块完成赋值,当中可以进行try catch异常处理。
public class Example{
	public static void main(String[] args)throws Exception{
		A.getLuckyNumber();
	}
}

class A{
	//若属性是非静态的,还可以在构造方法中写try-catch,在方法签名后面throws
	static int i;
	static {
		try{
			i = getLuckyNumber();
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}

	public static int getLuckyNumber()throws Exception{
		int lucky = (int)(Math.random()*10);
		if(lucky == 2 || lucky == 4) throw new Exception("Unlucky Number Exception");
		return lucky;
	}
}
  • 当父类的方法没有任何throws声明的时候,子类的方法去覆盖父类方法时,能够抛出各种运行时异常,但是没有意义。因为所有类默认抛出所有的运行时异常(即每个方法屁股后面都有看不见的尾巴:throws RuntimeException)。

  • 为了保证连续的多个资源都能够正常的去关闭,我们需要掌握try catch finally当中嵌套try catch,确保它们在某个出现异常的情况下,其他不出现异常的资源也会关闭。(请注意:学JDBC的时候需要用到这种语法~)

public class Example{
	public static void main(String[] args){
		SLT s1 = new SLT();
		SLT s2 = new SLT();
		SLT s3 = new SLT();

		try{
			s1.close();
		}
		catch(Exception e){
			System.out.println(e.getMessage());
		}
		finally{
			try{
				s2.close();
			}
			catch(Exception e){
				System.out.println(e.getMessage());
			}
			finally{
				try{
					s3.close();
				}
				catch(Exception e){
					System.out.println(e.getMessage());
				}
			}
		}
	}
}
class SLT{
	public void close()throws Exception{
		int rdNum = (int)(Math.random() * 2);
		System.out.println(rdNum);
		if(rdNum % 2 == 0) throw new Exception("冻住了");
		System.out.println("关上了");
	}
}
  • 在某些场景下,学会使用异常处理代替传统的分支和判断,会有奇效。
//判断传入的字符串是否为纯数字
public class TestExceptionPlus5{
	public static void main(String[] args){
		check("12345s")
	}
	public static boolean check(String str){
		try{
			Integer.parseInt(str);
			return true;
		}catch(Exception e){
			return false;
		}
	}
}

猜你喜欢

转载自blog.csdn.net/qq_30257081/article/details/84639960