Java 高级部分

java 高级部分

一 Java GUI 入门

1.基础概念

  1. GUI ( graphics user interface ):图形界面用户接口,Java 用来做界面的一套 API ( application program interface )
  2. 程序界面有两种
    • 控制台程序:console()
      System.out.println();
      
    • 窗口界面
      在这里插入图片描述

2.窗口开发

  1. 容器
    在这里插入图片描述
  2. 基本组件
    在这里插入图片描述

3.基本界面

  1. 做一个窗口
    • 编写一个类,找准一个父类 JFrame
    • 编写一个构造方法,设置一些属性
      public class Wnd1 extends JFrame{
      	JLabel lblAccount = new JLabel("账号:");
      	JTextField jtfAccount = new JTextField(25);
      	JLabel lblPwd = new JLabel("密码:");
      	JPasswordField jtfPwd = new JPasswordField(25);
      	JButton btnLogin = new JButton("登录");
      	JButton btnCalcel = new JButton("取消");
      	/*
      	JButton jb1 = new JButton("东");
      	JButton jb2 = new JButton("南");
      	JButton jb3 = new JButton("西");
      	JButton jb4 = new JButton("北");
      	JButton jb5 = new JButton("中");*/
      	JPanel jp1 = new JPanel();
      	JPanel jp2 = new JPanel();
      	JPanel jp4 = new JPanel();
      	JPanel jp3 = new JPanel();
      	
      	public Wnd1(){
      		//设置窗口的一些特性:
      		//1、设置大小
      		this.setSize(400,300);
      		
      	 	//2、设置标题
      		this.setTitle("QQ登录");
      		
      	 	//3、设置可见
      	 	this.setVisible(true);
      	 	
      		//4、设置可以被关闭
      		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      		
      	 	//5、设置窗口的位置,相对桌面居中
      	 	this.setLocationRelativeTo(null);
      	 	
      		//6、设置不能改变大小
      		this.setResizable(false);
      		
      		//放入我们的组件
      		initComponent();
      	}
      	
      	/**
      	 *初始化各个组件
      	 */
      	 public void initComponent(){
      		jp1.add(lblAccount);
      		jp1.add(jtfAccount);
      		jp2.add(lblPwd);
      		jp2.add(jtfPwd);
      		jp4.add(btnLogin);
      		jp4.add(btnCalcel);
      		jp3.add(jp1);
      		jp3.add(jp2);
      		jp3.add(jp4);
      		
      		// 将jp3放在窗口的中间.
      		this.add(jp3, "Center");
      		
      		// 事件处理者
      		MyHandler myHandler = new MyHandler();
      		// 将事件源和事件处理者关联
      		btnLogin.addActionListener(myHandler);
      		
      		// 将组件放入窗口受布局的影响,我们JFrame的默认布局是边框布局。
      		/*this.add(jb5,"Center");
      		this.add(jb1, "East");
      		this.add(jb2, "South");
      		this.add(jb3,"West");
      		this.add(jb4,"North");*/
      	}
      	
      	/**
      	 *事件处理者
      	 *ActionListener:按钮的点击事件接口
      	*/
      	class MyHandler implements ActionListener {
      		@Override
      		public void actionPerformed(ActionEvent e) {
          		// 获取账号,密码信息
          		String account = Wnd1.this.jtfAccount.getText();
          		String pwd = jtfPwd.getText();
          		
          		if("admin".equals(account) && "123".equals(pwd)){
              		System.out.println("登录成功!");
              		Wnd2 wnd2 = new Wnd2();
              		// 释放窗口
              		dispose();
          		}else{
              		System.out.println("账号或密码错误!");
              		
              		// null:表示是这个消息框的父窗口
              		JOptionPane.showMessageDialog(Wnd1.this,"账号或密码错误!");
          		}
      		}
      	}
      	
      	public static void main(String[] args) {
      		new Wnd1();
      	}
      }
      
      public class Wnd2 extends JFrame{
      	public Wnd2(){
      		this.setSize(800,400);
      		this.setVisible(true);
      	}
      }
      
  2. 开发组件
    在这里插入图片描述
    • 登录界面开发
      在这里插入图片描述
    • 组件布局
      • 边框布局 BorderLayout
        在这里插入图片描述
      • 流水布局 FlowLayout,从左往右,从上往下的布局
        JPanel:默认是流水布局的
        在这里插入图片描述
  3. 事件处理
    • 点击按钮,需要有所反应
  4. 观察者模式
    • 事件源:发生事件的对象
    • 事件处理者:实现了 ActionListener 接口的类的对象
    • 事件源.addActionListener(事件处理者)

4.Swing 高级组件

  1. 表格组件
    • JTable
  2. 构造方法
    在这里插入图片描述
    /**
     * 构造一个指定行和列的空表格
     */
    JTable(int,int)
    
    /**
     * 第一个参数为 Object 类型的二维数组,第一维表示行,第二维表示
     * 每行的具体数据
     * 第二个参数为 Object 类型的一维数组,表示每列的列表名
     */
    JTable(Object[][],Object[])
    
    /**
     * 使用模型来构造表格,TableModel 是一个接口,我们可以
     * 自己写一个类实现这个接口,从而完成模型的功能
     * 
     * Java 也帮我们提供了一个默认的表模型,DefaultTableModel
     * DefaultTableModel mdl=new DefaultTableModel(vRows,vColums);
     * JTable myTable=new JTable(mdl);
     */
    JTable(TableModel model)
    
  3. 新增行
    void addRow(Object[] rowData)	// 在模型的末尾添加一行
    void addRow(Vector rowData)		// 在模型的末尾添加一行
    
  4. 删除行
    void removeRow(int row)		// 从模型中删除 row 的行
    
  5. 代码
    public class JTableDemo1 extends JFrame{
    	JTable myTable;
    	
    	public JTableDemo1(){
    		this.setSize(500,400);
        	this.setTitle("我的数据表");
        	this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        	this.setLocationRelativeTo(null);
        	initComponent();
        	this.setVisible(true);
    	}
    	
    	public void initComponent(){
        	Object[] columns = {
                	"编号",
                	"姓名",
                	"性别",
                	"职位"
        	};
        	Object[][] rowData = {
                	{1,"张飞","男","打手"},
                	{2,"关羽","男","打手"},
                	{3,"孔明","男","军事"},
                	{4,"小乔","女","小三"},
        	};
        	
        	// 将表格放入窗口中
        	myTable = new JTable(rowData, columns);
        	// 设置单元格的高
        	myTable.setRowHeight(30);
        	// 将表格放入一个滚动界面中
        	this.add(new JScrollPane(myTable));
    	}
    	
    	public static void main(String[] args){
    		new JTableDemo1();
    	}
    }
    
    public class JTableDemo2 extends JFrame{
    	JTable myTable;
    	
    	public JTableDemo2(){
        	this.setSize(500,400);
        	this.setTitle("我的数据表");
        	this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        	this.setLocationRelativeTo(null);
        	initComponent();
        	this.setVisible(true);
    	}
    	
    	public void initComponent(){
        	// 表示是列名称
        	Vector vColumns = new Vector();
        	vColumns.add("编号");
        	vColumns.add("姓名");
        	vColumns.add("性别");
        	vColumns.add("职位");
        	
        	//表示行的数据
        	Vector vRows = new Vector();
        	Vector v1 = new Vector();
        	v1.add(1);
        	v1.add("张飞");
        	v1.add("男");
        	v1.add("打手");
        	vRows.add(v1);
        	Vector v2 = new Vector();
        	v2.add(2);
        	v2.add("张飞");
        	v2.add("男");
        	v2.add("打手");
        	vRows.add(v2);
        	Vector v3 = new Vector();
        	v3.add(3);
        	v3.add("张飞");
        	v3.add("男");
        	v3.add("打手");
        	vRows.add(v3);
        	Vector v4 = new Vector();
        	v4.add(4);
        	v4.add("张飞");
        	v4.add("男");
        	v4.add("打手");
        	vRows.add(v4);
        	
        	// 不加入这个模型的话,后续进行增删改有问题
        	DefaultTableModel mdl = new DefaultTableModel(vRows, vColumns);
        	// 将表格放入窗口中
        	myTable = new JTable(mdl);
        	// 设置单元格的高
        	myTable.setRowHeight(30);
        	// 将表格放入一个滚动界面中
        	this.add(new JScrollPane(myTable));
    	}
    	
    	public static void main(String[] args) {
        	new JTableDemo2();
    	}
    }
    
    public class JTableDemo3 extends JFrame{
    	JTable myTable;
    	JButton btnAdd = new JButton("新增");
    	JButton btnDel = new JButton("删除");
    	JPanel pnl = new JPanel();
    	private DefaultTableModel mdl;
    	
    	public JTableDemo3(){
        	this.setSize(500,400);
        	this.setTitle("我的数据表");
        	this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        	this.setLocationRelativeTo(null);
        	initComponent();
        	this.setVisible(true);
    	}
    	
    	public void initComponent(){
        	pnl.add(btnAdd);
        	pnl.add(btnDel);
        	this.add(pnl,"South");
        	
        	btnAdd.addActionListener(new ActionListener() {
            	@Override
            	public void actionPerformed(ActionEvent e) {
                	AddEmpFrame addEmpFrame = new AddEmpFrame(mdl);
            	}
        	});
        	btnDel.addActionListener(new ActionListener() {
            	@Override
            	public void actionPerformed(ActionEvent e) {
            		// 点击是:0,否:1,取消:2,×:-1
                	int i = JOptionPane.showConfirmDialog(null, "确认删除吗?");
                	if(i==0){
                    	// 获取表格的选中的行
                    	int selectedRow = myTable.getSelectedRow();
                    	if(selectedRow==-1){
                        	JOptionPane.showMessageDialog(null,"没有选中任何行!");
                        	return;
                    	}
                    	mdl.removeRow(selectedRow);
                	}
            	}
        	});
    
        	//表示是列名称
        	Vector vColumns = new Vector();
        	vColumns.add("编号");
        	vColumns.add("姓名");
        	vColumns.add("性别");
        	vColumns.add("职位");
        	
        	//表示行的数据
        	Vector vRows = new Vector();
        	Vector v1 = new Vector();
        	v1.add(1);
        	v1.add("张飞");
        	v1.add("男");
        	v1.add("打手");
        	vRows.add(v1);
        	Vector v2 = new Vector();
        	v2.add(2);
        	v2.add("张飞");
        	v2.add("男");
        	v2.add("打手");
        	vRows.add(v2);
        	Vector v3 = new Vector();
        	v3.add(3);
        	v3.add("张飞");
        	v3.add("男");
        	v3.add("打手");
        	vRows.add(v3);
        	Vector v4 = new Vector();
        	v4.add(4);
        	v4.add("张飞");
        	v4.add("男");
        	v4.add("打手");
        	vRows.add(v4);
        	
        	// 不加入这个模型的话,后续进行增删改有问题
        	mdl = new DefaultTableModel(vRows, vColumns);
        	// 将表格放入窗口中
        	myTable = new JTable(mdl);
        	// 设置单元格的高
        	myTable.setRowHeight(30);
        	// 将表格放入一个滚动界面中
        	this.add(new JScrollPane(myTable));
    	}
    	
    	public static void main(String[] args) {
        	new JTableDemo3();
    	}
    }
    
    class AddEmpFrame extends JFrame{
    	DefaultTableModel mdl;
    	JLabel lbl1 = new JLabel("编号:");
    	JLabel lbl2 = new JLabel("姓名:");
    	JLabel lbl3 = new JLabel("性别:");
    	JLabel lbl4 = new JLabel("职业:");
    	JTextField jtf1 = new JTextField(25);
    	JTextField jtf2 = new JTextField(25);
    	JTextField jtf3 = new JTextField(25);
    	JTextField jtf4 = new JTextField(25);
    	JButton btnSave = new JButton("保存");
    	JButton btnClose = new JButton("关闭");
    	JPanel pn1 = new JPanel();
    	JPanel pn2 = new JPanel();
    	JPanel pn3 = new JPanel();
    	JPanel pn4 = new JPanel();
    	JPanel pn5 = new JPanel();
    	JPanel pn6 = new JPanel();
    	
    	public  AddEmpFrame(DefaultTableModel mdl){
        	this.mdl = mdl;
        	this.setSize(400,500);
        	this.setTitle("新增员工");
        	this.initComponent();
        	this.setResizable(false);
        	this.setLocationRelativeTo(null);
        	this.setVisible(true);
    	}
    	
    	public void initComponent(){
        	pn1.add(lbl1);
        	pn1.add(jtf1);
        	pn2.add(lbl2);
        	pn2.add(jtf2);
        	pn3.add(lbl3);
        	pn3.add(jtf3);
        	pn4.add(lbl4);
        	pn4.add(jtf4);
        	pn5.add(btnSave);
        	pn5.add(btnClose);
        	pn6.add(pn1);
        	pn6.add(pn2);
        	pn6.add(pn3);
        	pn6.add(pn4);
        	pn6.add(pn5);
        	this.add(pn6);
        	
        	btnSave.addActionListener(new ActionListener() {
            	@Override
            	public void actionPerformed(ActionEvent e) {
                	Vector v = new Vector();
                	v.add(jtf1.getText());
                	v.add(jtf2.getText());
                	v.add(jtf3.getText());
                	v.add(jtf4.getText());
                	mdl.addRow(v);
            	}
        	});
        	btnClose.addActionListener(new ActionListener() {
            	@Override
            	public void actionPerformed(ActionEvent e) {
                	dispose();
            	}
        	});
    	}
    }
    

5.Swing 图形绘制

  1. 核心对象 Graphics
    • Graphics:当成一只笔
      • public abstract class Graphics extends Object{ // 代码体 }
      • Graphics 类是所有图形上下文的抽象基类,允许应用程序可以在组件(已知在各种设备上实现),以及闭屏图像上,进行绘制
      • Graphics 对象封装了 Java 支持的基本呈现操作所需的状态信息
    • 对象的获取
      • Graphics g=this.getGraphics();
      • 利用这个对象可以在窗口中,按钮上进行绘图
  2. 重绘
    • 当窗口发生改变以后,窗口会被重绘,只会重绘自己的一些窗口特征,而我们自己绘制的内容没有被重绘的,我们需要重写 paint() 方法,在这个方法中重绘
      @OVerride
      public void paint(Graphics g){
      	super.paint(g);
      	
      	// 绘制你自己的图形
      	for(int i=0;i<lines.size(){
      		Line line=(Line) lines.get(i);
      		g.drawLine(line.x1,line.y1,line.x2,line.y2);
      	}
      }
      
  3. 绘图操作
    /**
     * Toolkit:工具包
     * Toolkit.getDefaultToolkit():获取默认工具包
     */
    Image img=Toolkit.getDefaultToolkit().getImage("pName.png");
    

二 Java 常用类

资料
JAVA_API_CN.exe
JavaAPI 学习(java.lang).pdf
java笔记.txt

1.Object

2.包装类

资料
基本数据类型、包装类型和 String 间的转换

在这里插入图片描述

  1. 包装类
    • 对于基本数据类型的封装,封装称为了对象,方便操作
  2. 字符串 --> 基本数据类型、包装类
    • Integer 包装类的 public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型
    • 类似地,使用 java.lang 包中的 Byte、Short、Long、Float、Double 类调用相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型
  3. 基本数据类型、包装类 --> 字符串
    • 调用 String 类的 public static String valueOf(int a n) 可将 int 型转换为字符串
    • 相应的 valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b) 可由参数的相应类型到字符串的转换
  4. 字符数组 --> 字符串
    • String 类的构造器:String(char[] c) 和 String(char[] c,int offset,int length) 分别用字符数组中的全部字符和部分字符创建字符串对象
  5. 字符串 --> 字符数组
    • public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法
    • public void getChars(int srcBegin,int srcEnd,char[] dst,int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法
  6. 字节数组 --> 字符串
    • String(byte[] b):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的字符串
    • String(byte[] b,int offset,int length):用指定的字节数组的一部分,即从数组起始位置 offset 开始取 length 个字节构造一个字符串对象
  7. 字符串 --> 字节数组
    • public byte[] getBytes():使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中
    • public byte[] getBytes(String charsetName):使用指定的字符集将此 String 编码到byte 序列,并将结果存储到新的 byte 数组

3.String 类

  1. 字符串的本质是字符数组,具有不可变性

  2. 字符串的构造

    • String(byte[] bytes):构造一个新的 String,方法是使用平台的默认字符集解码字节的指定数组
    • String(byte[] bytes,String charsetName):构造一个新的 String,方法是使用指定的字符集解码指定的字节数组
    • 适合于在各种编码中进行转换
  3. String 的常用 API

    方法原型说明
    boolean equalsIgnoreCase(String anotherString)判断字符串 anotherString 是否与当前字符串相等,忽略大小写模式
    int compareTo(String anotherString根据 ASCII 码比较字符串 anotherString 和当前字符串的大小,比较方式类似于 c 语言中的 strcmp 函数
    boolean startsWith(String prefix)判断当前字符串是否以字符串 prefix 为开头
    boolean endsWith(String suffix)判断当前字符串是否以字符串 suffix 为后缀
    int indexOf(int ch)搜索字符 ch 在当前字符串中第一次出现的索引,没有出现则返回 -1
    int indexOf(String str)搜索字符串 str 在当前字符串中第一次出现的索引,没有出现则返回 -1
    int lastIndexOf(int ch)搜索字符 ch 在当前字符串中最后一次出现的索引,没有出现则返回 -1
    int lastIndexOf(String str)搜索字符串 str 在当前字符串中最后一次出现的索引,没有出现则返回 -1
    char cahrAt(int index)用于从指定位置提取单个字符,该位置由 index 指定,索引值必须为非负数
    String substring(in index)用于提取从 index 指定的位置开始的字符串部分
    String substring(int begin,int end)用于提取 begin 和 end 位置之间的字符串部分
    String concat(String str)用于连接两个字符串,并新建一个包含调用字符串的字符串对象
    String replace(char oldChar,char newChar)用于将调用字符串中出现的 oldChar 指定的字符全部替换为 newChar 指定的字符
    replaceAll(String regex,String replacement)用于将调用字符串中出现或者匹配 regex 的字符串全部都替换为 replacement 指定的字符
    String trim()用于返回一个前后不含任何空格的调用字符串的副本
  4. 正则表达式(用于验证指定字符串是否合法的一个模板)

    // s:你要验证的字符串,表达式:(匹配公式)
    boolean bFlag=s.matches(表达式);
    
    • 单个字符

      标识符说明
      某个单词(例如:1)直接匹配
      .匹配任意的字符
      [0-9]数组1-9,在里面时
      ^表达式必须以表达式的规则为开头
      表达式$必须以表达式的规则为结尾
    • 字符个数

      标识符次数
      *0次或者多次
      +1次或者多次
      0次或者1次
      {n}恰好 n 次
      {n, }n 次到无限
      {n,m}从 n 次到 m 次
    • 快捷符号

      标识符说明
      \d表示 [0-9]
      \D表示 [^ 0-9]
      \w表示 [0-9A-Z_a-z]
      \W表示[^0-9A-Z_a-z]
      \s表示 [\t \n \r]
      \S表示 [^ \t \n \r \f]
  5. 正则表达式作用

    • 用于验证手机号码、邮箱、生日等
    • 过滤
      • replaceAll()
    • 拆分
      • split()

4.StringBuffer 与 StringBuilder

  1. 可变字符串,当大量的对于字符串做操作,可以使用
  2. 代表可变的字符序列,JDK1.0 中声明,可以对字符串内容进行增删,此时不会产生新的对象
  3. 作为参数传递时,方法内部可以改变值
  4. StringBuffer 类不同于 String ,其对象必须使用构造器生成
    • StringBuffer():初始容量为 16 的字符串缓冲区
    • StringBuffer(int size):构造指定容量的字符串缓冲区
    • StringBuffer(String str):将内容初始化为指定字符串内容
  5. StringBuffer 类的常用方法
    /**
     * 当 append 和 insert 时,如果原来 value 数组长度不够,可扩容
     */
    StringBuffer append(xxx);	// 提供了很多的 append() 方法,用于进行字符串拼接
    StringBuffer insert(int offset,xxx);	// 在指定位置插入 xxx
    StringBuffer delete(int start,int end);	// 删除指定位置的内容
    StringBuffer replace(int start,int end,String str);	// 把 [start,end) 位置替换成 str
    StringBuffer reverse();	// 把当前字符串序列逆转
    
    int indexOf(String str);
    char charAt(int n);
    void setCharAt(int n,char ch);
    int length();
    String substring(int start,int end);
    
  6. StringBuilder 类
    • StringBuilder 和 StringBuffer 非常相似,均代表可变的字符序列,而且提供相关功能的方法也一样
  7. 面试题:对比 String、StringBuffer、StringBuilder
    • String(JDK1.0):不可变字符序列
    • StringBuffer(JDK1.0):可变字符序列、效率低、线程安全
    • StringBuilder(JDK5.0):可变字符序列、效率高、线程不安全
  8. 注意:作为参数传递的话,方法内部 String 不会改变其值,StringBuffer 和 StringBuilder 会改变其值

5.Math 类

  1. 里面都是静态常量和静态方法:用于对数学做一些计算用
  2. 一些常用方法
    public static double ceil(double a){  // 代码体 }
    public static double floor(double a){  // 代码体 }
    public static int round(float a){  // 代码体 }
    public static long round(double a){  // 代码体 }	// -12.5会出现错误
    
    public static double pow(double a,double b){  // 代码体 }
    
  3. Math.round() 四舍五入方法

三 Java 集合

资料
java集合框架.pdf
Java集合框架2.pdf
第11章节_Java集合.doc
第11章_Java集合.pptx
HashMap面试题.pdf
深入java8的集合3:HashMap的实现原理.pdf
java集合框架Over.pdf

1.集合框架的概述

  1. 集合、数组都是对多个数据进行存储操作的结构,简称 Java 容器
    • 说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储( .txt, .jpg , .avi ,数据库中)
  2. 数组在存储多个数据方面的特点
    • 一旦初始化以后,其长度就确定了
    • 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了
    • 比如:String[] arr; int[] arr1; Object[] arr2
  3. 数组在存储多个数据方面的缺点
    • 一旦初始化以后,其长度就不可修改
    • 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高
    • 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
    • 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足
  4. 集合框架
    • Collection 接口:单列集合,用来存储一个一个的对象
      • List 接口:存储有序的、可重复的数据 --> “动态”数组
        • ArrayList 、LinkedList 、Vector
      • Set 接口:存储无序的、不可重复的数据 --> 高中讲的“集合”
        • HashSet 、LinkedHashSet 、TreeSet
    • Map 接口:双列集合,用来存储一对(key - value)一对的数据 --> 高中函数:y = f(x)
      • HashMap:作为 Map 的主要实现类;线程不安全的,效率高;存储 null 的 key 和 value
        • LinkedHashMap:保证在遍历 map 元素时,可以按照添加的顺序实现遍历
      • TreeMap:保证按照添加的 key-value 对进行排序,实现排序遍历,此时考虑 key 的自然排序或定制排序
      • Hashtable:作为古老的实现类;线程安全的,效率低;不能存储 null 的 key 和 value
        • Properties:常用来处理配置文件,key 和 value 都是 String 类型

2.Collection

  1. Collection 接口方法
    方法说明
    boolean add(Object obj)将元素 e 添加到集合中
    boolean addAll(Collection coll)将 coll 集合中的元素添加到当前的集合中
    int size()获取有效元素的个数
    void clear()清空集合元素
    boolean isEmpty()判断当前集合是否为空
    boolean contains(Object obj)是通过元素的 equals() 来判断是否是同一个对象
    boolean containsAll(Collection c)也是调用元素的equals方法来比较的,拿两个集合的元素挨个比较
    boolean remove(Object obj)通过元素的 equals() 判断是否是要删除的那个元素,只会删除找到的第一个元素
    boolean removeAll(Collection coll)取当前集合的差集
    boolean retainAll(Collection c)把交集的结果存在当前集合中,不影响c
    boolean equals(Object obj)集合是否相等,要想返回 true ,需要当前集合和形参集合的元素都相同
    Object[] toArray()转成对象数组
    int hashCode()获取集合对象的哈希值
    Iterator<E> iterator()返回迭代器对象,用于集合遍历
  2. Iterator 迭代器接口
    • Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素
    • Collection 接口继承了 java.lang.Iterable 接口,该接口有一个 iterator() 方法,那么所
      有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象
    • Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力,如果需要创建
      Iterator 对象,则必须有一个被迭代的集合
    • 集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合
      的第一个元素之前
  3. Iterator 接口的方法
    方法说明
    boolean hasNext()判断是否还有下一个元素
    E next()①指针下移 ②将下移以后集合位置上的元素返回
    default void remove()Iterator 可以删除集合的元素,但是是遍历过程中通过迭代器对象的 remove 方法,不是集合对象的 remove 方法
  4. 注意
    • 在调用 next() 方法之前必须要调用 hasNext() 进行检测。若不调用,且
      下一条记录无效,直接调用 next() 会抛出 NoSuchElementException 异常
    • 如果还未调用 next() 或在上一次调用 next 方法之后已经调用了 remove 方法,
      再调用 remove 都会报 IllegalStateException
  5. foreach 循环
    • Java 5.0 提供了 foreach 循环迭代访问 Collection 和数组
    • 遍历操作不需获取 Collection 或数组的长度,无需使用索引访问元素
    • 遍历集合的底层调用 Iterator 完成操作
    • foreach 还可以用来遍历数组
①List 接口
  1. List 接口框架

    • Collection 接口:单列集合,用来存储一个一个的对象
      • List 接口:存储有序的、可重复的数据 --> “动态”数组,替换原有的数组
        • ArrayList:作为 List 接口的主要实现类;线程不安全的,效率高;底层使用 Object[] elementData 存储
        • LinkedList:对于频繁的插入、删除操作,使用此类效率比 ArrayList 高;底层使用双向链表存储
        • Vector:作为 List 接口的古老实现类;线程安全的,效率低;底层使用 Object[] elementData 存储
  2. ArrayList 的源码分析

    1. jdk 7 情况下
      • ArrayList list = new ArrayList(); // 底层创建了长度是 10 的 Object[] 数组 elementData
      • list.add(123); // elementData[0] = new Integer(123);
      • list.add(11); // 如果此次的添加导致底层 elementData 数组容量不够,则扩容
      • 默认情况下,扩容为原来的容量的 1.5 倍,同时需要将原有数组中的数据复制到新的数组中
      • 结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
    2. jdk 8 中 ArrayList 的变化
      • ArrayList list = new ArrayList(); //底层 Object[] elementData 初始化为 {} ,并没有创建长度为 10 的数组
      • list.add(123); // 第一次调用 add() 时,底层才创建了长度 10 的数组,并将数据 123 添加到 elementData[0]
      • 后续的添加和扩容操作与 jdk 7 无异
    3. 小结:jdk7 中的 ArrayList 的对象的创建类似于单例的饿汉式,而 jdk8 中的 ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存
  3. LinkedList 的源码分析

    • LinkedList list = new LinkedList(); // 内部声明了 Node 类型的 first 和 last 属性,默认值为 null
    • list.add(123); // 将 123 封装到 Node 中,创建了 Node 对象
    • 其中,Node 定义为:体现了 LinkedList 的双向链表的说法
      private static class Node<E> {
          Eitem;
          Node<E> next;
          Node<E> prev;
      
          Node(Node<E> prev, E element, Node<E> next) {
      	    this.item = element;
      	    this.next = next;
      	    this.prev = prev;
          }
      }
      
  4. Vector 的源码分析

    • jdk7 和 jdk8 中通过 Vector() 构造器创建对象时,底层都创建了长度为 10 的数组。在扩容方面,默认扩容为原来的数组长度的 2 倍
  5. List 接口

    • 常用方法

      List 接口方法说明
      void add(int index,Object ele)在 index 位置插入 ele 元素
      boolean addAll(index,Collection eles)从 index 位置开始将 eles 中的所有元素添加进来
      Object get(int index)获取指定 index 位置的元素
      int indexOf(Object obj)返回 obj 在集合中首次出现的位置
      int lastIndexOf(Object obj)返回 obj 在当前集合中末次出现的位置
      Object remove(int index)移除指定 index 位置的元素,并返回此元素
      Object set(index,Object ele)设置指定 index 位置的元素为 ele
      List subList(int fromIndex,int toIndex)返回从 fromIndex 到 toIndex 位置的子集合
    • 总结:常用方法

      • 增:add(Object obj)
      • 删:remove(int index) / remove(Object obj)
      • 改:set(int index, Object ele)
      • 查:get(int index)
      • 插:add(int index, Object ele)
      • 长度:size()
      • 遍历:
        • Iterator 迭代器方式
        • 增强 for 循环
        • 普通的循环
  6. ArrayList 实现类

    • Arrays.asList(…):方法返回的 List 集合,既不是 ArrayList 实例,也不是
      Vector 实例,Arrays.asList(…) 返回值是一个固定长度的 List 集合
  7. LinkedList 实现类

    • 新增方法
      新增方法说明
      void addFirst(Object obj)插入列表头部
      void addLast(Object obj)插入列表尾部
      Object getFirst()获取头部元素
      Object getLast()获取尾部元素
      Object removeFirst()移除头部元素
      Object removeLast()移除尾部元素
    • 双向链表,内部没有声明数组,而是定义了 Node 类型的 first 和 last ,用于记录首末元素。同时,定义内部类 Node ,作为 LinkedList 中保存数据的基本结构。Node 除了保存数据,还定义了两个变量
      • prev 变量记录前一个元素的位置
      • next 变量记录下一个元素的位置
    • 代码
      private static class Node<E> {
      	E item;
      	Node<E> next;
      	Node<E> prev;
      	Node(Node<E> prev, E element, Node<E> next) {
      		this.item = element;
      		this.next = next;
      		this.prev = prev;
      	}
      }
      
  8. Vector 实现类

    • Vector 是一个古老的集合,JDK1.0 就有了。大多数操作与 ArrayList 相同,区别之处在于 Vector 是线程安全的
    • 在各种 list 中,最好把 ArrayList 作为缺省选择。当插入、删除频繁时,使用 LinkedList;Vector 总是比 ArrayList 慢,所以尽量避免使用
    • 新增方法
      新增方法
      void addElement(Object obj)
      void insertElementAt(Object obj,int index)
      void setElementAt(Object obj,int index)
      void removeElement(Object obj)
      void removeAllElements()
  9. 面试题①:ArrayList、LinkedList、Vector 三者的异同?

    • 相同点:三个类都是实现了 List 接口,存储数据的特点相同:存储有序的、可重复的数据
    • 不同点:见上
  10. 面试题②:请问 ArrayList / LinkedList / Vector 的异同?谈谈你的理解?ArrayList 底层是什么?扩容机制?Vector 和 ArrayList 的最大区别?

    1. ArrayList 和 LinkedList 的异同:
      • 二者都线程不安全,相对线程安全的 Vector,执行效率高
      • 此外,ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。对于随机访问 get 和 set ,ArrayList 绝对优于 LinkedList ,因为 LinkedList 要移动指针。对于新增和删除操作 add(特指插入)和 remove ,LinkedList 比较占优势,因为 ArrayList 要移动数据
    2. ArrayList 和 Vector 的区别
      • Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步类(synchronized),属于强同步类。因此开销就比 ArrayList 要大,访问要慢。正常情况下,大多数的 Java 程序员使用 ArrayList 而不是 Vector ,因为同步完全可以由程序员自己来控制
      • Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍
      • Vector 还有一个子类 Stack
②Set 接口
  1. Set 接口的框架
    • Collection 接口:单列集合,用来存储一个一个的对象
      • Set 接口:存储无序的、不可重复的数据 --> 高中讲的“集合”
        • HashSet:作为 Set 接口的主要实现类;线程不安全的;可以存储 null 值
          • 作为 HashSet 的子类;遍历其内部数据时,可以按照添加的顺序遍历对于频繁的遍历操作,LinkedHashSet 效率高于 HashSet
        • TreeSet:可以按照添加对象的指定属性,进行排序
  2. Set 接口中没有额外定义新的方法,使用的都是 Collection 中声明过的方法
  3. 向Set(主要指:HashSet、LinkedHashSet )中添加的数据,其所在的类一定要重写 hashCode() 和 equals()
    • 要求①:重写的 hashCode() 和 equals() 尽可能保持一致性:相等的对象必须具有相等的散列码
    • 要求②:重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值
  4. Set:存储无序的、不可重复的数据
    以 HashSet 为例:
    • 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的
    • 不可重复性:保证添加的元素按照 equals() 判断时,不能返回 true。即:相同的元素只能添加一个
  5. 添加元素的过程
    以 HashSet 为例
    • 我们向 HashSet 中添加元素 a ,首先调用元素 a 所在类的 hashCode() 方法,计算元素 a 的哈希值,此哈希值接着通过某种算法计算出在 HashSet 底层数组中的存放位置(即为:索引位置),判断 数组此位置上是否已经有元素:
      • 如果此位置上没有其他元素,则元素a添加成功 —> 情况 1
      • 如果此位置上有其他元素 b (或以链表形式存在的多个元素),则比较元素 a 与元素 b 的 hash 值:
        • 如果 hash 值不相同,则元素 a 添加成功 —> 情况 2
        • 如果 hash 值相同,进而需要调用元素 a 所在类的 equals() 方法:
          • equals() 返回 true ,元素 a 添加失败
          • equals() 返回 false ,则元素 a 添加成功 —> 情况 2
    • 对于添加成功的情况 2 和情况 3 而言:元素 a 与已经存在指定索引位置上数据以链表的方式存储
      • jdk 7 :元素 a 放到数组中,指向原来的元素
      • jdk 8 :原来的元素在数组中,指向元素 a
      • 总结:七上八下
  6. HashSet 实现类
    • 不能保证元素的排列顺序
    • HashSet 不是线程安全的
    • 集合元素可以是 null
  7. LinkedHashSet 实现类
    • LinkedHashSet 是 HashSet 的子类
    • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的
    • LinkedHashSet 插入性能略低于 HashSet ,但在迭代访问 Set 里的全
      部元素时有很好的性能
    • LinkedHashSet 不允许集合元素重复
    • 图示:
      在这里插入图片描述
  8. TreeSet 实现类
    1. TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态
    2. TreeSet 底层使用红黑树结构存储数据
    3. 新增的方法(了解)
      方法
      Comparator comparator()
      Object first()
      Object last()
      Object lower(Object e)
      Object higher(Object e)
      SortedSet subSet(fromElement, toElement)
      SortedSet headSet(toElement)
      SortedSet tailSet(fromElement)
    4. TreeSet 两种排序方法:自然排序(添加的对象的类实现 Comparable 接口)和定制排序(创建 TreeSet时传入 Comparator )。默认情况下,TreeSet 采用自然排序
      • 自然排序中,比较两个对象是否相同的标准为:compareTo() 返回 0,不再是 equals()
      • 定制排序中,比较两个对象是否相同的标准为:compare() 返回 0,不再是 equals()
    5. TreeSet 采用红黑树的存储结构,有序,查询速度比 List 快

3.Map

  1. Map 的实现类的结构

    • Map:双列数据,存储 key-value 对的数据 —> 类似于高中的函数:y = f(x)
      • HashMap:作为 Map 的主要实现类;线程不安全的,效率高;存储 null 的 key 和 value
        • LinkedHashMap:保证在遍历 map 元素时,可以按照添加的顺序实现遍历
          原因:在原有的 HashMap 底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于 HashMap
      • TreeMap:保证按照添加的 key-value 对进行排序,实现排序遍历。此时考虑 key 的自然排序或定制排序
        底层使用红黑树
      • Hashtable:作为古老的实现类;线程安全的,效率低;不能存储 null 的 key 和 value
        • Properties:常用来处理配置文件。key 和 value 都是 String 类型
  2. Map 结构的理解

    • Map 中的 key :无序的、不可重复的,使用 Set 存储所有的 key —> key 所在的类要重写equals() 和 hashCode() (以HashMap为例)
    • Map 中的value:无序的、可重复的,使用 Collection 存储所有的 value —> value 所在的类要重写 equals()
    • 一个键值对:key-value 构成了一个 Entry 对象
    • Map 中的 entry:无序的、不可重复的,使用 Set 存储所有的 entry
      在这里插入图片描述
  3. 常用方法

    添加、删除、修改操作说明
    Object put(Object key,Object value)将指定 key-value 添加到(或修改)当前 map 对象中
    void putAll(Map m)将 m 中的所有 key-value 对存放到当前 map 中
    Object remove(Object key)移除指定 key 的 key-value 对,并返回 value
    void clear()清空当前 map 中的所有数据
    元素查询的操作说明
    Object get(Object key)获取指定 key 对应的 value
    boolean containsKey(Object key)是否包含指定的 key
    boolean containsValue(Object value)是否包含指定的 value
    int size()返回 map 中 key-value 对的个数
    boolean isEmpty()判断当前 map 是否为空
    boolean equals(Object obj)判断当前 map 和参数对象 obj 是否相等
    元视图操作说明
    Set keySet()返回所有 key 构成的 Set 集合
    Collection values()返回所有 value 构成的 Collection 集合
    Set entrySet()返回所有 key-value 对构成的 Set 集合
①HashMap 与 LinkedHashMap
  1. HashMap 的底层实现原理(以 jdk7 为例说明)
    • HashMap map = new HashMap(); 在实例化以后,底层创建了长度是16的一维数组 Entry[] table
    • …可能已经执行过多次put…
    • map.put(key1,value1);
    • 首先,调用 key1 所在类的 hashCode() 计算 key1 哈希值,此哈希值经过某种算法计算以后,得到在 Entry 数组中的存放位置
    • 如果此位置上的数据为空,此时的 key1-value1 添加成功 --> 情况 1
    • 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较 key1 和已经存在的一个或多个数据的哈希值
      • 如果 key1 的哈希值与已经存在的数据的哈希值都不相同,此时 key1-value1 添加成功 --> 情况 2
      • 如果 key1 的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用 key1 所在类的 equals(key2) 方法,比较
        • 如果 equals() 返回 false,此时 key1-value1 添加成功 --> 情况 3
        • 如果 equals() 返回 true,使用 value1 替换 value2
    • 补充:关于情况 2 和情况 3 :此时 key1-value1 和原来的数据以链表的方式存储
    • 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的 2 倍,并将原有的数据复制过来
  2. HashMap 中 jdk8 相较于 jdk7 在底层实现方面的不同
    • new HashMap():底层没有创建一个长度为 16 的数组
    • jdk 8底层的数组是:Node[],而非 Entry[]
    • 首次调用 put() 方法时,底层创建长度为 16 的数组
    • jdk7 底层结构只有:数组+链表。jdk8 中底层结构:数组+链表+红黑树
      • 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
      • 当数组的某一个索引位置上的元素以链表形式存在的 数据个数 > 8 且 当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储
  3. HashMap 的特点
    • 允许使用 null 键和 null 值,与 HashSet 一样,不保证映射的顺序
    • 所有的 key 构成的集合是 Set :无序的、不可重复的。所以,key 所在的类要重写:equals() 和 hashCode()
    • 所有的 value 构成的集合是 Collection:无序的、可以重复的。所以,value 所在的类
      要重写:equals()
    • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,
      hashCode 值也相等
    • HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true
      在这里插入图片描述
  4. HashMap 源码中的重要常量
    常量说明
    DEFAULT_INITIAL_CAPACITYHashMap 的默认容量,16
    MAXIMUM_CAPACITYHashMap 的最大支持容量,2^30
    DEFAULT_LOAD_FACTORHashMap 的默认加载因子
    TREEIFY_THRESHOLDBucket 中链表长度大于该默认值,转化为红黑树
    UNTREEIFY_THRESHOLDBucket 中红黑树存储的 Node 小于该默认值,转化为链表
    MIN_TREEIFY_CAPACITY桶中的 Node 被树化时最小的 hash 表容量。(当桶中 Node 的数量大到需要变红黑树时,若 hash 表容量小于 MIN_TREEIFY_CAPACITY 时,此时应执行 resize 扩容操作这个 MIN_TREEIFY_CAPACITY 的值至少是 TREEIFY_THRESHOLD 的 4 倍)
    table存储元素的数组,总是 2 的 n 次幂
    entrySet存储具体元素的集
    sizeHashMap 中存储的键值对的数量
    modCountHashMap 扩容和结构改变的次数
    threshold扩容的临界值 = 容量 * 填充因子
    loadFactor填充因子
  5. HashMap 面试题①
    负载因子值的大小,对 HashMap 有什么影响
    • 负载因子的大小决定了 HashMap 的数据密度
    • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降
    • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间
    • 按照其他语言的参考及研究经验,会考虑将负载因子设置为 0.7~0.75,此时平均检索长度接近于常数
  6. HashMap 注意点
    1. HashMap 的扩容
      • 当 HashMap 中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对 HashMap 的数组进行扩容,而在 HashMap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize
    2. HashMap 什么时候进行扩容和树形化
      • 当 HashMap 中的其中一个链的对象个数如果达到了 8 个,此时如果 capacity 没有达到 64 ,那么 HashMap 会先扩容解决,如果已经达到了 64 ,那么这个链会变成树,结点类型由 Node 变成 TreeNode 类型。当然,如果当映射关系被移除后,下次 resize 方法时判断树的结点个数低于 6 个,也会把树再转为链表
    3. 关于映射关系的 key 是否可以修改
      • 映射关系存储到 HashMap 中会存储 key 的 hash 值,这样就不用在每次查找时重新计算每一个 Entry 或 Node(TreeNode)的 hash 值了,因此如果已经 put 到 Map 中的映射关系,再修改 key 的属性,而这个属性又参与 hashcode 值的计算,那么会导致匹配不上
  7. LinkedHashMap 特点
    • LinkedHashMap 是 HashMap 的子类
    • 在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
    • 与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致
  8. LinkedHashMap 中的内部类:Entry
    static class Entry<K,V> extends HashMap.Node<K,V> {
    	Entry<K,V> before, after;
    	Entry(int hash, K key, V value, Node<K,V> next) {
    		super(hash, key, value, next);
    	}
    }
    
②TreeMap
  1. TreeMap 存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态
  2. TreeMap 底层使用红黑树结构存储数据
  3. TreeMap 的 Key 的排序
    • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口
  4. TreeMap 判断两个 key 相等的标准:两个 key 通过 compareTo() 方法或者 compare() 方法返回 0
③Hashtable 与 Properties
  1. Hashtable
    • Hashtable 是个古老的 Map 实现类,JDK1.0 就提供了。不同于 HashMap,Hashtable 是线程安全的
    • Hashtable 实现原理和 HashMap 相同,功能相同。底层都使用哈希表结构,查询
      速度快,很多情况下可以互用
    • 与 HashMap 不同,Hashtable 不允许使用 null 作为 key 和 value
    • 与 HashMap 一样,Hashtable 也不能保证其中 Key-Value 对的顺序
    • Hashtable 判断两个 key 相等、两个 value 相等的标准,与 HashMap 一致
  2. Properties
    • Properties 类是 Hashtable 的子类,该对象用于处理属性文件
    • 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
    • 存取数据时,建议使用 setProperty(String key,String value) 方法和 getProperty(String key) 方法

4.Collections 工具类

  1. Collections 是一个操作 Set、List 和 Map 等集合的工具类

  2. Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

  3. 常用方法

    排序操作(均为 static 方法)说明
    void reverse(List list)反转 List 中元素的顺序
    void shuffle(List list)对 List 集合元素进行随机排序
    void sort(List list)根据元素的自然顺序对指定 List 集合元素按升序排序
    void sort(List list , Comparator comp)根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
    void swap(List list , int i , int j)将指定 list 集合中的 i 处元素和 j 处元素进行交换
    查找、替换(均为 static 方法)说明
    Object max(Collection coll)根据元素的自然顺序,返回给定集合中的最大元素
    Object max(Collection coll , Comparator comp)根据 Comparator 指定的顺序,返回给定集合中的最大元素
    Object min(Collection coll)根据元素的自然顺序,返回给定集合中的最小元素
    Object min(Collection coll , Comparator comp)根据 Comparator 指定的顺序,返回给定集合中的最小元素
    int frequency(Collection coll , Object obj)返回指定集合中指定元素的出现次数
    void copy(List dest , List src)将 src 中的内容复制到 dest 中
    boolean replaceAll(List list , Object oldVal , Object newVal)使用新值替换 List 对象的所有旧值
  4. Collections 同步控制

    • Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

四 泛型

  1. 所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)
  2. 从 JDK1.5 以后,Java 引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时再指定集合元素的类型,正如:List<String>,这表明该 List 只能保存字符串类型的对象
  3. JDK1.5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参

1.在集合中使用泛型

  1. 在集合中使用泛型之前的情况

    • 问题①:类型不安全
    • 问题②:强转时,可能出现 ClassCastException
  2. 在集合中使用泛型的情况

    • 编译时,就会进行类型检查,保证数据的安全
    • 避免了强转操作
    • JDK7 新特性:类型推断
      Map<String,Integer> map = new HashMap<>();
      
  3. 集合接口或集合类在 JDK5.0 时都修改为带泛型的结构

  4. 在实例化集合类时,可以指明具体的泛型类型

  5. 指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型

    • 比如:add(E e) —>实例化以后:add(Integer e)
  6. 注意点:泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换

  7. 如果实例化时,没有指明泛型的类型。默认类型为 java.lang.Object 类型

2.自定义泛型结构

  1. 自定义泛型类、接口
    1. 泛型的声明
      interface GenericsInterface<T>{}
      class GenericsClass<T>{}
      
    2. 体会:使用泛型的主要优点是能够在编译时而不是在运行时检测错误
    3. 注意点:
      • 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
      • 泛型类的构造器如下:public GenericClass(){},而下面是错误的:public GenericClass<E>(){}
      • 泛型如果不指定,将被擦除,泛型对应的类型均按照 Object 处理,但不等价于 Object 。经验:泛型要使用一路都用。要不用,一路都不要用
      • 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象
      • 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型
      • 异常类不能是泛型的
      • 不能使用 new E[]。但是可以:E[] elements = (E[])new Object[capacity];
        参考:ArrayList源码中声明:Object[] elementData ,而非泛型参数类型数组
    4. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型
      class Father<T1, T2> {
      
      }
      
      
      // 子类不保留父类的泛型(此时子类不是泛型类)
      // (1)没有类型 擦除
      class Son1 extends Father {// 等价于class Son extends Father<Object,Object>
      
      }
      // (2)具体类型
      class Son2 extends Father<Integer, String> {
      
      }
      
      
      // 子类保留父类的泛型(此时子类是泛型类)
      // (1)全部保留
      class Son3<T1, T2> extends Father<T1, T2> {
      
      }
      // (2)部分保留
      class Son4<T2> extends Father<Integer, T2> {
      
      }
      
      class Father<T1, T2> {
      
      }
      
      
      // 子类不保留父类的泛型(此时子类不是泛型类)
      // (1)没有类型 擦除
      class Son<A, B> extends Father{//等价于class Son<A,B> extends Father<Object,Object>
      
      }
      // (2)具体类型
      class Son2<A, B> extends Father<Integer, String> {
      
      }
      
      
      // 子类保留父类的泛型(此时子类是泛型类)
      // (1)全部保留
      class Son3<T1, T2, A, B> extends Father<T1, T2> {
      
      }
      // (2)部分保留
      class Son4<T2, A, B> extends Father<Integer, T2> {
      
      }
      
    5. 泛型类的使用
      class Person<T> {
      	// 使用T类型定义变量
      	private T info;
      	
      	// 使用T类型定义构造器
      	public Person() {
      	
      	}
      	public Person(T info) {
      		this.info = info;
      	}
      	
      	
      	// 使用T类型定义一般方法
      	public T getInfo() {
      		return info;
      	}
      	public void setInfo(T info) {
      		this.info = info;
      	}
      	
      	// static的方法中不能声明泛型
      	/*public static void show(T t) {
      	
      	}*/
      	
      	// 不能在try-catch中使用泛型定义
      	/*public void test() {
      		try {
      		
      		} catch (MyException<T> ex) {
      		
      		}
      	}*/
      )
      
  2. 泛型方法
    1. 方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型
    2. 泛型方法的格式
      • [访问权限] <泛型> 返回类型 方法名( [泛型标识 参数名称] ) 抛出的异常
        public class DAO {
        	public <E> E get(int id, E e) {
        		E result = null;
        		return result;
        	}
        }
        
    3. 泛型方法声明泛型时也可以指定上限
    4. 总结
      • 在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系
      • 换句话说,泛型方法所属的类是不是泛型类都没有关系
      • 泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的,并非在实例化类时确定

3.泛型在继承上的体现

  1. 虽然类 A 是类 B 的父类,但是 G<A> 和 G<B> 二者不具备子父类关系,二者是并列关系
  2. 类A是类B的父类,A<G> 是 B<G> 的父类
  3. 示例
    public void testGenericAndSubClass() {
    	Object[] obj = null;
    	String[] str = null;
    	// Object[] 是 String[] 的父类,多态
    	obj = str;
    	Object o = str[0];
    	
    	// 在泛型的集合上
    	List<Person> list1 = null;
    	List<Man> list2 = null;
    	// list1 = list2;(报错)
    }
    

4.通配符的使用

  1. 通配符:?
  2. 类 A 是类B的父类,G<A> 和 G<B> 是没有关系的,二者共同的父类是:G<?>
  3. 通配符的使用
    • 读取 List<?> 的对象 list 中的元素时,永远是安全的,因为不管 list 的真实类型是什么,它的父类都是 Object
    • 写入 list 中的元素时,不行。因为我们不知道 c 的元素类型,我们不能向其中添加对象
      • 唯一的例外是 null ,它是所有类型的成员
    • 示例
      public static void main(String[] args) {
      	List<?> list = null;
      	
      	list = new ArrayList<String>();
      	list = new ArrayList<Double>();
      	
      	// list.add(3);//编译不通过
      	list.add(null);
      	
      	List<String> l1 = new ArrayList<String>();
      	List<Integer> l2 = new ArrayList<Integer>();
      	l1.add("尚硅谷");
      	l2.add(15);
      	read(l1);
      	read(l2);
      }
      
      
      public static void read(List<?> list) {
      	for (Object o : list) {
      		System.out.println(o);
      	}
      }
      
  4. 注意点
    //注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?
    public static <?> void test(ArrayList<?> list){
    }
    
    //注意点2:编译错误:不能用在泛型类的声明上
    class GenericTypeClass<?>{
    }
    
    //注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象
    ArrayList<?> list2 = new ArrayList<?>();
    
  5. 有限制的通配符
    • <?>:允许所有泛型的引用调用
    • extends:通配符指定上限,使用时指定的类型必须是继承某个类,或者实现某个接口,即 <=
    • super:通配符指定下限,使用时指定的类型不能小于操作的类,即 >=
  6. 举例(有限制的通配符)
    • <? extends Number> (无穷小 , Number]:只允许泛型为 Number 及 Number 子类的引用调用
    • <? super Number> [Number , 无穷大):只允许泛型为 Number 及 Number 父类的引用调用
    • <? extends Comparable>:只允许泛型为实现 Comparable 接口的实现类的引用调用
    • 代码:
      @Test
      public void test4(){
      
          List<? extends Person> list1 = null;
          List<? super Person> list2 = null;
      
          List<Student> list3 = new ArrayList<Student>();
          List<Person> list4 = new ArrayList<Person>();
          List<Object> list5 = new ArrayList<Object>();
      
          list1 = list3;
          list1 = list4;
          // 编译失败
      	// list1 = list5;
      
      	// 编译失败
      	// list2 = list3;
          list2 = list4;
          list2 = list5;
      
          // 读取数据:
          list1 = list3;
          Person p = list1.get(0);
          //编译失败
          // Student s = list1.get(0);
      
          list2 = list4;
          Object obj = list2.get(0);
          // 编译失败
      	// Person obj = list2.get(0);
      
          //写入数据:
          //编译失败
      	// list1.add(new Student());
      
          //编译通过
          list2.add(new Person());
          list2.add(new Student());
      }
      

五 IO 流

资料
第13章_IO流.pptx
第13章节练习_IO流.doc
计算机字符编码.pdf
拓展:装饰设计模式.pdf

1.File 类的使用

  1. File 类的使用

    • java.io.File 类:文件和文件目录路径的抽象表示形式,与平台无关
    • File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用 输入/输出流
    • 想要在 Java 程序中表示一个真实存在的文件或目录,那么必须有一个 File 对象,但是 Java 程序中的一个 File 对象,可能没有一个真实存在的文件或目录
    • File 对象可以作为参数传递给流的构造器
  2. 常用构造器

    构造器说明
    public File(String pathname)以 pathname 为路径创建 File 对象,可以是绝对路径或者相对路径,如果 pathname 是相对路径,则默认的当前路径在系统属性 user.dir 中存储
    public File(String parent,String child)以 parent 为父路径,child 为子路径创建 File 对象
    public File(File parent,String child)根据一个父 File 对象和子文件路径创建 File 对象
  3. 路径分隔符

    • 路径中的每级目录之间用一个路径分隔符隔开
    • 路径分隔符和系统有关
      • windows 和 DOS 系统默认使用 “\” 来表示
      • UNIX 和 URL 使用 “/” 来表示
    • Java 程序支持跨平台运行,因此路径分隔符要慎用
    • 为了解决这个隐患,File 类提供了一个常量
      • public static final String separator:根据操作系统,动态的提供分隔符
        File file1 = new File("d:\\info.txt");
        File file2 = new File("d:" + File.separator + "info.txt");
        File file3 = new File("d:/info.txt");
        
  4. 常用方法

    File 类的获取功能说明
    public String getAbsolutePath()获取绝对路径
    public String getPath()获取路径
    public String getName()获取名称
    public String getParent()获取上层文件目录路径,若无,返回 null
    public long length()获取文件长度(即:字节数),不能获取目录的长度
    public long lastModified()获取最后一次的修改时间,毫秒值
    public String[] list()获取指定目录下的所有文件或者文件目录的名称数组
    public File[] listFiles()获取指定目录下的所有文件或者文件目录的 File 数组
    File 类的重命名功能说明
    public boolean renameTo(File dest)把文件重命名为指定的文件路径
    File 类的判断功能说明
    public boolean isDirectory()判断是否是文件目录
    public boolean isFile()判断是否是文件
    public boolean exists()判断是否存在
    public boolean canRead()判断是否可读
    public boolean canWrite()判断是否可写
    public boolean isHidden()判断是否隐藏
    File 类的创建功能说明
    public boolean createNewFile()创建文件,若文件存在,则不创建,返回false
    public boolean mkdir()创建文件目录,如果此文件目录存在,就不创建了,如果此文件目录的上层目录不存在,也不创建
    public boolean mkdirs()创建文件目录,如果上层文件目录不存在,一并创建
  • 注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目
    路径下
    File 类的删除功能说明
    public boolean delete()删除文件或者文件夹
  • 删除注意事项:
    • Java 中的删除不走回收站
    • 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录
  1. 说明
    在这里插入图片描述

2.IO 流原理及流的分类

  1. Java IO 原理
    • I/O 是 Input/Output 的缩写, I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如 读/写 文件,网络通讯等
    • Java 程序中,对于数据的 输入/输出 操作以“流(stream)” 的方式进行
    • java.io 包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据
    • 图示
      在这里插入图片描述
  2. 流的分类
    • 按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)
    • 按数据流的流向不同分为:输入流,输出流
    • 按流的角色的不同分为:节点流,处理流
      (抽象基类)字节流处理流
      输入流InputStreamReader
      输出流OutputStreamWriter
    • 补充:
      • 节点流和处理流
        • 节点流:直接从数据源或目的地读写数据
        • 处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能
      • Java 的 IO 流共涉及 40 多个类,实际上非常规则,都是从 4 个抽象基类派生的
      • 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀
    • 图示
      在这里插入图片描述
  3. IO 流体系
    分类字节输入流字节输出流字符输入流字符输出流
    抽象基类InputStreamOutputStreamReaderWriter
    访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
    访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
    访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
    访问字符串StringReaderStringWriter
    缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
    转换流InputStreamReaderOutputStreamWriter
    对象流ObjectInputStreamObjectOutputStream
    过滤流FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
    打印流PrintStreamPrintWriter
    推回输入流PushbackInputStreamPushbackReader
    特殊流DataInputStreamDataOutputStream

3.节点流(文件流)

  1. InputStream & Reader
    • InputStream 和 Reader 是所有输入流的基类
    • InputStream(典型实现:FileInputStream)
    • Reader(典型实现:FileReader)
    • 程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件 IO 资源
    • FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream 用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader
    • InputStream
      方法说明
      int read()从输入流中读取数据的下一个字节(返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1)
      int read(byte[] b)从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中,如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数
      int read(byte[] b, int off,int len)将输入流中最多 len 个数据字节读入 byte 数组,尝试读取 len 个字节,但读取的字节也可能小于该值,以整数形式返回实际读取的字节数,如果因为流位于文件末尾而没有可用的字节,则返回值 -1
      public void close() throws IOException关闭此输入流并释放与该流关联的所有系统资源
    • Reader
      方法说明
      int read()读取单个字符(作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2 个字节的 Unicode 码),如果已到达流的末尾,则返回 -1)
      int read(char[] cbuf)将字符读入数组,如果已到达流的末尾,则返回 -1,否则返回本次读取的字符数
      int read(char[] cbuf,int off,int len)将字符读入数组的某一部分,存到数组 cbuf 中,从 off 处开始存储,最多读 len 个字符,如果已到达流的末尾,则返回 -1,否则返回本次读取的字符数
      public void close() throws IOException关闭此输入流并释放与该流关联的所有系统资源
  2. OutputStream & Writer
    • 因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组,即以 String 对象作为参数
    • FileOutputStream 用于写出非文本数据之类的原始字节流,要写出字符流,需要使用 FileWriter
    • OutputStream
      方法说明
      void write(int b)将指定的字节写入此输出流(write 的常规协定是:向输出流写入一个字节,要写入的字节是参数 b 的八个低位,b 的 24 个高位将被忽略, 即写入 0~255 范围的)
      void write(byte[] b)将 b.length 个字节从指定的 byte 数组写入此输出流(write(b) 的常规协定是:应该与调用 write(b, 0, b.length) 的效果完全相同)
      void write(byte[] b,int off,int len)将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流
      public void flush()throws IOException刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标
      public void close() throws IOException关闭此输出流并释放与该流关联的所有系统资源(需要先刷新,再关闭此流)
    • Writer
      方法说明
      void write(int c)写入单个字符(要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入 0 到 65535 之间的 Unicode 码)
      void write(char[] cbuf)写入字符数组
      void write(char[] cbuf,int off,int len)写入字符数组的某一部分,从 off 开始,写入 len 个字符
      void write(String str)写入字符串
      void write(String str,int off,int len)写入字符串的某一部分
      void flush()刷新该流的缓冲,则立即将它们写入预期目标
      public void close() throws IOException关闭此输出流并释放与该流关联的所有系统资源(需要先刷新,再关闭此流)
  3. 注意点
    • 在写入一个文件时,如果使用构造器 FileOutputStream(file) ,则目录下有同名文件将被覆盖
    • 如果使用构造器 FileOutputStream(file,true) ,则目录下的同名文件不会被覆盖,在文件内容末尾追加内容
    • 在读取文件时,必须保证该文件已存在,否则报异常
    • 字节流操作字节,比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt
    • 字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt ,.java ,.c ,.cpp 等语言的源代码,尤其注意 .doc ,excel ,ppt 这些不是文本文件
    • 文本文件如果只是单纯的复制,并不需要在内存中查看,那么可以使用字节流来进行操作,而非文本文件不可以使用字符流来进行复制

4.缓冲流

  1. 为了提高数据读写的速度,Java API 提供了带缓冲功能的流类,在使用这些流类时,会创建一个内部缓冲区数组,缺省使用 8192 个字节(8Kb)的缓冲区
    public class BufferedInputStream extends FilterInputStream{
    	private static int DEFAULT_BUFFER_SIZE=8192;
    }
    
  2. 缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为
    • BufferedInputStream 和 BufferedOutputStream
    • BufferedReader 和 BufferedWriter
  3. 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区
  4. 当使用 BufferedInputStream 读取字节文件时,BufferedInputStream 会一次性从文件中读取 8192 个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个 8192 个字节数组(读取时还需要准备 byte[] 来进行存入)
  5. 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满, BufferedOutputStream 才会把缓冲区中的数据一次性写到文件里,使用方法 flush() 可以强制将缓冲区的内容全部写入输出流(写时还需要从 byte[] 来进行读取)
  6. 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也
    会相应关闭内层节点流
  7. flush()方法的使用:手动将buffer中内容写入文件
  8. 如果是带缓冲区的流对象的 close() 方法,不但会关闭流,还会在关闭流之前刷
    新缓冲区,关闭后不能再写出
  9. 示例:
    BufferedReader br = null;
    BufferedWriter bw = null;
    try {
    	// 创建缓冲流对象:它是处理流,是对节点流的包装
    	br = new BufferedReader(new FileReader("d:\\IOTest\\source.txt"));
    	bw = new BufferedWriter(new FileWriter("d:\\IOTest\\dest.txt"));
    	String str;
    	while ((str = br.readLine()) != null) { // 一次读取字符文本文件的一行字符
    		bw.write(str); // 一次写入一行字符串
    		bw.newLine(); // 写入行分隔符
    	}
    	bw.flush(); // 刷新缓冲区
    } catch (IOException e) {
    	e.printStackTrace();
    } finally {
    	// 关闭IO流对象
    	try {
    		if (bw != null) {
    			bw.close(); // 关闭过滤流时,会自动关闭它所包装的底层节点流
    		}
    	} catch (IOException e) {
    		e.printStackTrace();
    	}
    	try {
    		if (br != null) {
    			br.close();
    		}
    	} catch (IOException e) {
    		e.printStackTrace();
    	}
    }
    

5.转换流

  1. 转换流提供了在字节流和字符流之间的转换
  2. Java API 提供了两个转换流
    • InputStreamReader:将 InputStream 转换为 Reader
    • OutputStreamWriter:将 Writer 转换为 OutputStream
  3. 字节流中的数据都是字符时,转成字符流操作更高效
  4. 很多时候我们使用转换流来处理文件乱码问题,实现编码和解码的功能
  5. InputStreamReader
    • 实现将字节的输入流按指定字符集转换为字符的输入流

    • 需要和 InputStream “套接”

    • 构造器:

      构造器
      public InputStreamReader(InputStream in)
      public InputStreamReader(InputStream in,String charsetName)
  6. OutputStreamWriter
    • 实现将字符的输出流按指定字符集转换为字节的输出流
    • 需要和 OutputStream “套接”
    • 构造器:
      构造器
      public OutputStreamWriter(OutputStream out)
      public OutputStreamWriter(OutputStream out,String charsetName)
  7. 图示:
    在这里插入图片描述
字符编码
  1. 编码表的由来
    • 计算机只能识别二进制数据,早期由来是电信号,为了方便应用计算机,让它可以识别各个国家的文字,就将各个国家的文字用数字来表示,并一一对应,形成一张表,这就是编码表
  2. 常见的编码表
    • ASCII:美国标准信息交换码
      • 用一个字节的7位可以表示
    • ISO8859-1:拉丁码表,欧洲码表
      • 用一个字节的8位表示
    • GB2312:中国的中文编码表,最多两个字节编码所有字符
    • GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多两个字节编码
    • Unicode:国际标准码,融合了目前人类使用的所有字符,为每个字符分配唯一的字符码,所有的文字都用两个字节来表示
    • UTF-8:变长的编码方式,可用1-4 个字节来表示一个字符
  3. Unicode 不完美,这里就有三个问题(Unicode 在很长一段时间内无法推广,直到互联网的出现)
    • 我们已经知道,英文字母只用一个字节表示就够了
    • 如何才能区别 Unicode 和 ASCII ?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢
    • 如果和 GBK 等双字节编码方式一样,用最高位是 1 或 0 表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符
  4. 面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF8 就是每次 8 个位传输数据,而 UTF-16 就是每次16 个位,这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了
  5. Unicode 只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案,推荐的 Unicode 编码是 UTF-8 和 UTF-16
  6. 图示:
    在这里插入图片描述

6.其他流

①标准输入输出流
  1. System.in 和 System.out 分别代表了系统标准的输入和输出设备
  2. 默认输入设备是:键盘,输出设备是:显示器
  3. System.in 的类型是 InputStream
  4. System.out 的类型是 PrintStream,其是 OutputStream 的子类FilterOutputStream 的子类
  5. 重定向:通过 System 类的 setIn,setOut 方法对默认设备进行改变
    public static void setIn(InputStream in)
    
    public static void setOut(PrintStream out)
    
  6. 示例:
    /**
     * 从键盘输入字符串,要求将读取到的整行字符串转成大写输出,然后继续
     * 进行输入操作,直至当输入“e”或者“exit”时,退出程序
     */
    System.out.println("请输入信息(退出输入e或exit):");
    // 把"标准"输入流(键盘输入)这个字节流包装成字符流,再包装成缓冲流
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    String s = null;
    try {
    	while ((s = br.readLine()) != null) { // 读取用户输入的一行数据 --> 阻塞程序
    		if ("e".equalsIgnoreCase(s) || "exit".equalsIgnoreCase(s)) {
    			System.out.println("安全退出!!");
    			break;
    		}
    		// 将读取到的整行字符串转成大写输出
    		System.out.println("-->:" + s.toUpperCase());
    		System.out.println("继续输入信息");
    	}
    } catch (IOException e) {
    	e.printStackTrace();
    } finally {
    	try {
    		if (br != null) {
    			br.close(); // 关闭过滤流时,会自动关闭它包装的底层节点流
    		}
    	} catch (IOException e) {
    		e.printStackTrace();
    	}
    }
    
②打印流
  1. 实现将基本数据类型的数据格式转化为字符串输出
  2. 打印流:PrintStream 和 PrintWriter
    • 提供了一系列重载的 print() 和 println() 方法,用于多种数据类型的输出
    • PrintStream 和 PrintWriter 的输出不会抛出 IOException 异常
    • PrintStream 和 PrintWriter 有自动 flush 功能
    • PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节,在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类
    • System.out 返回的是 PrintStream 的实例
  3. 示例:
    PrintStream ps = null;
    try {
    	FileOutputStream fos = new FileOutputStream(new File("D:\\IO\\text.txt"));
    	// 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
    	ps = new PrintStream(fos, true);
    	if (ps != null) {// 把标准输出流(控制台输出)改成文件
    		System.setOut(ps);
    	}
    	for (int i = 0; i <= 255; i++) { // 输出ASCII字符
    		System.out.print((char) i);
    		if (i % 50 == 0) { // 每50个数据一行
    			System.out.println(); // 换行
    		}
    	}
    } catch (FileNotFoundException e) {
    	e.printStackTrace();
    } finally {
    	if (ps != null) {
    		ps.close();
    	}
    }
    
③数据流
  1. 为了方便地操作 Java 语言的基本数据类型和 String 的数据,可以使用数据流

  2. 数据流有两个类:(用于读取和写出基本数据类型、String类的数据)

    • DataInputStream 和 DataOutputStream
    • 分别“套接”在 InputStream 和 OutputStream 子类的流上
  3. 常用方法

    DataInputStream 常用方法DataOutputStream 常用方法
    byte readByte()byte writeByte()
    short readShort()short writeShort()
    int readInt()int writeInt()
    long readLong()long writeLong()
    float readFloat()float writeFloat()
    double readDouble()double writeDouble()
    char readChar()char writeChar()
    boolean readBoolean()boolean writeBoolean()
    String readUTF()String writeUTF()
    void readFully(byte[] b)void writeFully(byte[] b)
  4. 示例:

    DataOutputStream dos = null;
    try { // 创建连接到指定文件的数据输出流对象
    	dos = new DataOutputStream(new FileOutputStream("destData.dat"));
    	dos.writeUTF("我爱北京天安门"); // 写UTF字符串
    	dos.writeBoolean(false); // 写入布尔值
    	dos.writeLong(1234567890L); // 写入长整数
    	System.out.println("写文件成功!");
    } catch (IOException e) {
    	e.printStackTrace();
    } finally { // 关闭流对象
    	try {
    		if (dos != null) {
    			// 关闭过滤流时,会自动关闭它包装的底层节点流
    			dos.close();
    		}
    	} catch (IOException e) {
    		e.printStackTrace();
    	}
    }
    
    /**
     *读取的顺序要和写入的顺序一致,否则会报错
     */
    DataInputStream dis = null;
    try {
    	dis = new DataInputStream(new FileInputStream("destData.dat"));
    	String info = dis.readUTF();
    	boolean flag = dis.readBoolean();
    	long time = dis.readLong();
    	System.out.println(info);
    	System.out.println(flag);
    	System.out.println(time);
    } catch (Exception e) {
    	e.printStackTrace();
    } finally {
    	if (dis != null) {
    		try {
    			dis.close();
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    }
    

7.对象流

  1. 对象流
    • ObjectInputStream 和 OjbectOutputSteam
    • 用于存储和读取基本数据类型数据或对象的处理流,它的强大之处就是可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来
    • 序列化:用 ObjectOutputStream 类保存基本类型数据或对象的机制
    • 反序列化:用 ObjectInputStream 类读取基本类型数据或对象的机制
    • ObjectOutputStream 和 ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量
  2. 对象的序列化
    1. 对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点,当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象
    2. 序列化的好处在于可将任何实现了 Serializable 接口的对象转化为字节数据,使其在保存和传输时可被还原
    3. 序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础,因此序列化机制是 JavaEE 平台的基础
    4. 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一,否则,会抛出 NotSerializableException 异常
      • Serializable
      • Externalizable(有需要被实现的方法)
    5. 凡是实现 Serializable 接口的类都有一个表示序列化版本标识符的静态变量
      • private static final long serialVersionUID;
      • serialVersionUID 用来表明类的不同版本间的兼容性,简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容
      • 如果类没有显示定义这个静态常量,它的值是 Java 运行时环境根据类的内部细节自动生成的,若类的实例变量做了修改,serialVersionUID 可能发生变化,故建议,显式声明
    6. 简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的,在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)
  3. 使用对象流序列化对象
    • 若某个类实现了 Serializable 接口,该类的对象就是可序列化的
      • 创建一个 ObjectOutputStream
      • 调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象
      • 注意写出一次,操作 flush() 一次
    • 反序列化
      • 创建一个 ObjectInputStream
      • 调用 readObject() 方法读取流中的对象
    • 强调:如果某个类的属性不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化
  4. 面试题:
    谈谈你对 java.io.Serializable 接口的理解,我们知道它用于序列化,是空方法接口,还有其它认识吗?
    • 实现了 Serializable 接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子,这一过程亦可通过网络进行,这意味着序列化机制能自动补偿操作系统间的差异,换句话说,可以先在 Windows 机器上创建一个对象,对其序列化,然后通过网络发给一台 Unix 机器,然后在那里准确无误地重新“装配”,不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节
    • 由于大部分作为参数的类如 String 、Integer 等都实现了 java.io.Serializable 的接口,也可以利用多态的性质,作为参数使接口更灵活

8.随机存取文件流

  1. RandomAccessFile 声明在 java.io 包下,但直接继承于 java.lang.Object 类,并且它实现了 DataInput、DataOutput 这两个接口,也就意味着这个类既可以读也可以写
  2. RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意地方来读、写文件
    • 支持只访问文件的部分内容
    • 可以向已存在的文件后追加内容
  3. RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置, RandomAccessFile 类对象可以自由移动记录指针
    • long getFilePointer():获取文件记录指针的当前位置
    • void seek(long pos):将文件记录指针定位到 pos 位置
  4. 构造器
    • public RandomAccessFile(File file, String mode)
    • public RandomAccessFile(String name, String mode)
  5. 创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式
    • r:以只读方式打开
    • rw:打开以便读取和写入
    • rwd:打开以便读取和写入,同步文件内容的更新
    • rws:打开以便读取和写入,同步文件内容和元数据的更新
  6. 如果模式为只读 r ,则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常, 如果模式为 rw 读写,如果文件不存在则会去创建文件,如果存在则不会创建
  7. 用处:
    • 我们可以用 RandomAccessFile 这个类,来实现一个多线程断点下载的功能,下载前都会建立两个临时文件,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能

9.NIO.2 中 Path、Paths、Files 类的使用

  1. Java NIO 概述

    • Java NIO (New IO,Non-Blocking IO)是从 Java 1.4 版本开始引入的一套新的IO API,可以替代标准的 Java IO API,NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区的(IO 是面向流的)、基于通道的 IO 操作,NIO 将以更加高效的方式进行文件的读写操作
    • Java API 中提供了两套 NIO ,一套是针对标准输入输出 NIO ,另一套就是网络编程 NIO
    • |----java.nio.channels.Channel
      • |----FileChannel:处理本地文件
      • |----SocketChannel:TCP 网络编程的客户端的 Channel
      • |----ServerSocketChannel:TCP 网络编程的服务器端的 Channel
      • |-----DatagramChannel:UDP 网络编程中发送端和接收端的 Channel
  2. NIO. 2

    • 随着 JDK 7 的发布,Java 对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2,因为 NIO 提供的一些功能,NIO 已经成为文件处理中越来越重要的部分
  3. Path、Paths 和 Files 核心 API

    • 早期的 Java 只提供了一个 File 类来访问文件系统,但 File 类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息
    • NIO. 2 为了弥补这种不足,引入了 Path 接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置,Path 可以看成是 File 类的升级版本,实际引用的资源也可以不存在
    • 在以前 IO 操作都是这样写的
      import java.io.File;
      
      File file = new File("index.html");
      
    • 但在 Java7 中,我们可以这样写
      import java.nio.file.Path;
      import java.nio.file.Paths;
      
      Path path = Paths.get("index.html");
      
    • 同时,NIO.2 在 java.nio.file 包下还提供了 Files、Paths 工具类,Files 包含了大量静态的工具方法来操作文件,Paths 则包含了两个返回 Path 的静态工厂方法
    • Paths 类提供的静态 get() 方法用来获取 Path 对象
      • static Path get(String first, String … more):用于将多个字符串串连成路径
      • static Path get(URI uri):返回指定 uri 对应的 Path 路径
  4. Path 接口

    方法说明
    String toString()返回调用 Path 对象的字符串表示形式
    boolean startsWith(String path)判断是否以 path 路径开始
    boolean endsWith(String path)判断是否以 path 路径结束
    boolean isAbsolute()判断是否是绝对路径
    Path getParent()返回 Path 对象包含整个路径,不包含 Path 对象指定的文件路
    Path getRoot()返回调用 Path 对象的根路径
    Path getFileName()返回与调用 Path 对象关联的文件名
    int getNameCount()返回 Path 根目录后面元素的数量
    Path getName(int idx)返回指定索引位置 idx 的路径名称
    Path toAbsolutePath()作为绝对路径返回调用 Path 对象
    Path resolve(Path p)合并两个路径,返回合并后的路径对应的 Path 对象
    File toFile()将 Path 转化为 File 类的对象
  5. Files 类
    java.nio.file.Files 用于操作文件或目录的工具类

    方法说明
    Path copy(Path src, Path dest, CopyOption … how)文件的复制
    Path createDirectory(Path path, FileAttribute<?> … attr)创建一个目录
    Path createFile(Path path, FileAttribute<?> … arr)创建一个文件
    void delete(Path path)删除一个文件/目录,如果不存在,执行报错
    void deleteIfExists(Path path)Path对应的文件/目录如果存在,执行删除
    Path move(Path src, Path dest, CopyOption…how)将 src 移动到 dest 位置
    long size(Path path)返回 path 指定文件的大小
    方法(用于判断)说明
    boolean exists(Path path, LinkOption … opts)判断文件是否存在
    boolean isDirectory(Path path, LinkOption … opts)判断是否是目录
    boolean isRegularFile(Path path, LinkOption … opts)判断是否是文件
    boolean isHidden(Path path)判断是否是隐藏文件
    boolean isReadable(Path path)判断文件是否可读
    boolean isWritable(Path path)判断文件是否可写
    boolean notExists(Path path, LinkOption … opts)判断文件是否不存在
    方法(用于操作内容)说明
    SeekableByteChannel newByteChannel(Path path, OpenOption…how)获取与指定文件的连接,how 指定打开方式
    DirectoryStream<Path> newDirectoryStream(Path path)打开 path 指定的目录
    InputStream newInputStream(Path path, OpenOption…how)获取 InputStream 对象
    OutputStream newOutputStream(Path path, OpenOption…how)获取 OutputStream 对象

六 多线程

资料
第8章_多线程.pptx
第8章节练习_多线程.doc
Java多线程.pdf
Java多线程游戏开发.pdf

1.基本概念:程序、进程、线程

  1. 程序(program):是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象
  2. 进程(process):是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程:有它自身的产生、存在和消亡的过程——生命周期
    • 程序是静态的,进程是动态的
    • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
  3. 线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径
    • 若一个进程同一时间并行执行多个线程,就是支持多线程的
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间(方法区和堆共享) --> 它们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简便、高效,但多个线程操作共享的系统资源可能就会带来安全的隐患
  4. 单核 CPU 和多核 CPU 的理解
    • 单核 CPU ,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务
    • 一个 Java 应用程序 java.exe ,其实至少有三个线程:main() 主线程,gc() 垃圾回收线程,异常处理线程,如果发生异常,会影响主线程
  5. 并行与并发
    • 并行:多个 CPU 同时执行多个任务
    • 并发:一个 CPU (采用时间片)同时执行多个任务

2.线程的创建和使用

  1. JDK1.5 之前创建新执行线程有两种方法

    • 继承 Thread 类的方式
    • 实现 Runnable 接口的方式
  2. Thread 类

    构造器说明
    public Thread()创建新的 Thread 对象
    public Thread(String threadname)创建线程并指定线程实例名
    public Thread(Runnable target)指定创建线程的目标对象,它实现了 Runnable 接口中的 run 方法
    public Thread(Runnable target, String name)创建新的Thread对象
    方法说明
    void start()启动当前线程,调用当前线程的 run()
    void run()通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中
    String getName()获取当前线程的名字
    void setName(String name)设置当前线程的名字
    static Thread currentThread()返回当前线程,在 Thread 子类中就是 this ,通常用于主线程和 Runnable 实现类
    static void yield()释放当前 cpu 的执行权 ①暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 ②若队列中没有同优先级的线程,忽略此方法 ③让步以后,该线程可能会再次获得执行权
    void join()当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
    static void sleep(long millis)①令当前活动线程在指定时间段内放弃对CPU控制(此时该线程是阻塞状态),使其他线程有机会被执行,时间到后重排队 ②抛出 InterruptedException 异常(在继承 Thread 类的线程当中只能捕获,因为不能抛出比父类更大的异常)
    void stop()已过时,强制线程生命期结束,不推荐使用
    boolean isAlive()返回 boolean ,判断线程是否还活着
    int getPriority()返回线程优先值
    void setPriority(int newPriority)改变线程的优先级
  3. 继承 Thread 类

    1. 步骤:
      • 创建一个继承于 Thread 类的子类
      • 重写 Thread 类的 run() --> 将此线程执行的操作声明在 run() 中
      • 创建 Thread 类的子类的对象
      • 通过此对象调用 start():①启动当前线程 ②调用当前线程的 run()
    2. 注意点:
      • 如果自己手动调用 run() 方法,那么就只是普通方法,没有启动多线程模式
      • run() 方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU 调度决定
      • 想要启动多线程,必须调用 start()
      • 一个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”
  4. 实现 Runnable 接口

    1. 步骤:
      • 创建一个实现了 Runnable 接口的类
      • 实现类去实现 Runnable 中的抽象方法:run()
      • 创建实现类的对象
      • 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
      • 通过 Thread 类的对象调用 start():① 启动线程 ②调用当前线程的 run() --> 调用了 Runnable 类型的 target 的 run()
    2. 区别:
      • 继承 Thread:线程代码存放 Thread 子类 run() 方法中
      • 实现 Runnable:线程代码存在接口的子类的 run() 方法
  5. 为什么推荐实现 Runnable 接口的方式

    • 实现的方式没有类的单继承性的局限性
    • 实现的方式更适合来处理多个线程有共享数据的情况
    • 使用同步方法的时候还需要方法为静态的(因为锁的对象为 this),如果方法为静态的,那么牵扯到方法里面的属性也需要静态的
  6. 线程的优先级

    • 常用常量:(1~10)
      • MAX_PRIORITY:10
      • MIN _PRIORITY:1
      • NORM_PRIORITY:5
    • 说明:
      • 线程创建时继承父线程的优先级
      • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
  7. 线程的分类

    • Java 中的线程分为两类:一种是守护线程,一种是用户线程
    • 它们在几乎每个方面都是相同的,唯一的区别是判断 JVM 何时离开
    • 守护线程是用来服务用户线程的,通过在 start() 方法前调用
      thread.setDaemon(true) 可以把一个用户线程变成一个守护线程
    • Java 垃圾回收就是一个典型的守护线程
    • 若 JVM 中都是守护线程,当前 JVM 将退出

3.线程的生命周期

  1. JDK 中用 Thread.State 类定义了线程的几种状态
    • 新建: 当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
    • 就绪:处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件,只是没分配到 CPU 资源
    • 运行:当就绪的线程被调度并获得 CPU 资源时,便进入运行状态, run() 方法定义了线程的操作和功能
    • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
    • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
  2. 图示
    在这里插入图片描述

4.线程的同步

  1. Java 中,通过同步机制,来解决线程的安全问题
  2. 同步代码块
    • 代码
      synchronized(同步监视器){
      	//需要被同步的代码
      }
      
    • 说明
      • 操作共享数据的代码,即为需要被同步的代码 --> 不能包含代码多了,也不能包含代码少了
      • 共享数据:多个线程共同操作的变量
      • 同步监视器,俗称:锁
        • 任何一个类的对象,都可以充当锁
        • 多个线程必须要共用同一把锁
      • 在实现 Runnable 接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器
      • 在继承 Thread 类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器
  3. 同步方法
    • 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
    • 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
    • 同步方法的监视器
      • 非静态的同步方法,同步监视器是:this --> 适用于实现 Runnable 类的线程
      • 静态的同步方法,同步监视器是:当前类本身 --> 适用于继承 Thread 类的线程(此时方法类的使用的变量也应该是静态的)
  4. Lock(锁)
    • 从 JDK 5.0 开始,Java 提供了更强大的线程同步机制 --> 通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当
    • java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象
    • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显式加锁、释放锁
    • 代码
      class TestLock{
      	private final ReentrantLock lock = new ReenTrantLock();
      	public void test(){
      		lock.lock();
      		try{
      			//保证线程安全的代码;
      		}finally{
      			// 注意:如果同步代码有异常,要将 unlock() 写入 finally 语句块
      			lock.unlock();
      		}
      	}
      }
      
  5. synchronized 与 Lock 的对比
    • Lock 是显式锁(手动开启和关闭锁),synchronized 是隐式锁,出了作用域自动释放
    • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
    • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
  6. 优先使用顺序
    • Lock --> 同步代码块(已经进入了方法体,分配了相应资源)-- > 同步方法(在方法体之外)
  7. 死锁
    • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
    • 说明
      • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
      • 我们使用同步时,要避免出现死锁
    • 解决方法
      • 专门的算法、原则
      • 尽量减少同步资源的定义
      • 尽量避免嵌套同步
  8. 释放锁的操作
    • 当前线程的同步方法、同步代码块执行结束
    • 当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行
    • 当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception ,导致异常结束
    • 当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
  9. 不会释放锁的操作
    • 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行
    • 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(同步监视器)
      • 应尽量避免使用suspend()和resume()来控制线程
  10. 单例模式(懒汉式)
    public class Singleton{
    	private static Singleton instance = null;
    	
    	private Singleton(){
    	
    	}
    	
    	public static Singleton getInstance(){
    		if(instance==null){
    			synchronized(Singleton.class){
    				if(instance == null){
    					instance=new Singleton();
    				}
    			}
    		}
    		return instance;
    	}
    }
    

5.线程的通信

  1. 方法
    方法说明
    void wait()执行此方法,当前线程就进入阻塞状态,并释放同步监视器
    void notify()执行此方法,就会唤醒被 wait 的一个线程,如果有多个线程被 wait ,就唤醒优先级高的那个
    void notifyAll()执行此方法,就会唤醒所有被 wait 的线程
  2. 这三个方法只有在 synchronized 方法或 synchronized 代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException 异常
  3. 因为这三个方法必须由锁对象调用,而任意对象都可以作为 synchronized 的同步锁,因此这三个方法只能在 Object 类中声明
  4. wait() 方法
    • 在当前线程中调用方法: 对象名.wait()
    • 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify(或 notifyAll ) 为止
    • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
    • 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
    • 在当前线程被 notify 后,要重新获得监控权,然后从断点处继续代码的执行
  5. notify() / notifyAll()
    • 在当前线程中调用方法: 对象名.notify()
    • 功能:唤醒等待该对象监控权的一个 / 所有线程
    • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
  6. 面试题:sleep() 和 wait() 的异同?
    • 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
    • 不同点:
      • 两个方法声明的位置不同:Thread 类中声明 sleep() , Object 类中声明 wait()
      • 调用的要求不同:sleep() 可以在任何需要的场景下调用,wait() 必须使用在同步代码块或同步方法中
      • 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放
  7. 生产者 / 消费者问题
public class ProductTest {
	public static void main(String[] args) {
		Clerk clerk = new Clerk();
		Thread productorThread = new Thread(new Productor(clerk));
		Thread consumerThread = new Thread(new Consumer(clerk));
		productorThread.start();
		consumerThread.start();
	}
}


class Clerk {	// 售货员
	private int product = 0;
	
	public synchronized void addProduct() {
		if (product >= 20) {
			try {
				wait();
			} catch (InterruptedException e){
				e.printStackTrace();
			}
		} else {
			product++;
			System.out.println("生产者生产了第" + product + "个产品");
			notifyAll();
		}
	}
	
	public synchronized void getProduct() {
		if (this.product <= 0) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		} else {
			System.out.println("消费者取走了第" +product + "个产品");
			product--;
			notifyAll();
		}
	}
}


class Productor implements Runnable {	// 生产者
	Clerk clerk;
	
	public Productor(Clerk clerk) {
		this.clerk = clerk;
	}
	
	public void run() {
		System.out.println("生产者开始生产产品");
		while (true) {
			try {
				Thread.sleep((int) Math.random() * 1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			clerk.addProduct();
		}
	}
}


class Consumer implements Runnable {	// 消费者
	Clerk clerk;
	
	public Consumer(Clerk clerk) {
		this.clerk = clerk;
	}
	
	public void run() {
		System.out.println("消费者开始取走产品");
		while (true) {
			try {
				Thread.sleep((int) Math.random() * 1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			clerk.getProduct();
		}
	}
}

6.JDK5.0 新增线程创建方式

  1. 实现 Callable 接口
    1. 步骤
      • 创建一个实现 Callable 的实现类
      • 实现 call() 方法,将此线程需要执行的操作声明在 call() 中
      • 创建 Callable 接口实现类的对象
      • 将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象
      • 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()
      • 获取 Callable 中 call() 方法的返回值
        • get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值
    2. 与使用 Runnable 相比, Callable 功能更强大些
      • 相比 run() 方法,可以有返回值
      • 方法可以抛出异常
      • 支持泛型的返回值
      • 需要借助 FutureTask 类,比如获取返回结果
    3. Future 接口
      • 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等
      • FutrueTask 是 Futrue 接口的唯一的实现类
      • FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
  2. 线程池
    1. 步骤
      • 提供指定线程数量的线程池
      • 执行指定的线程的操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
      • 关闭连接池
    2. 好处
      • 提高响应速度(减少了创建新线程的时间)
      • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
      • 便于线程管理
        • corePoolSize:核心池的大小
        • maximumPoolSize:最大线程数
        • keepAliveTime:线程没有任务时最多保持多长时间后会终止
    3. JDK 5.0 起提供了线程池相关API:ExecutorService 和 Executors
    4. ExecutorService:真正的线程池接口,常见子类 ThreadPoolExecutor
      方法说明
      void execute(Runnable command)执行任务 / 命令,没有返回值,一般用来执行 Runnable
      <T> Future<T> submit(Callable<T> task)执行任务,有返回值,一般又来执行 Callable
      void shutdown()关闭连接池
    5. Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
      方法说明
      static ExecutorService newCachedThreadPool创建一个可根据需要创建新线程的线程池
      static ExecutorService newFixedThreadPool(int nThreads)创建一个可重用固定线程数的线程池
      static ExecutorService newSingleThreadExecutor()创建一个只有一个线程的线程池
      static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行

七 网络编程

资料
第14章_网络编程.pptx
Java网络编程.pdf

1.网络编程概述

  1. 网络通信协议
    • OSI 参考模型:模型过于理想化,未能在因特网上进行广泛推广
    • TCP/IP 参考模型(或 TCP/IP 协议):事实上的国际标准
    • 通信协议分层的思想
      • 在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式
      • 即同层间可以通信、上一层可以调用下一层,而与再下一层不发生关系。各层互不影响,利于系统的开发和扩展
    • 图示
      在这里插入图片描述
  2. IP
    • 唯一的标识 Internet 上的计算机(通信实体)
    • 本地回环地址(hostAddress):127.0.0.1 主机名(hostName):localhost
    • IP 地址分类方式 1:IPV4 和 IPV6
      • IPV4:4 个字节组成,4 个 0-255,大概 42 亿,30 亿都在北美,亚洲 4 亿,2011 年初已经用尽,以点分十进制表示,如 192.168.0.1
      • IPV6:128 位(16 个字节),写成 8 个无符号整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
    • IP 地址分类方式 2:公网地址(万维网使用)和私有地址(局域网使用)
  3. 端口号
    • 标识正在计算机上运行的进程(程序)
    • 不同的进程有不同的端口号
    • 被规定为一个 16 位的整数 0~65535
    • 端口分类
      • 公认端口:0~1023,被预先定义的服务通信占用(如:HTTP 占用端口 80 ,FTP 占用端口 21 ,Telnet 占用端口 23 )
      • 注册端口:1024~49151,分配给用户进程或应用程序(如:Tomcat 占用端口 8080 ,MySQL 占用端口 3306 ,Oracle 占用端口 1521 等)
      • 动态 / 私有端口:49152~65535
    • 端口号与 IP 地址的组合得出一个网络套接字:Socket
  4. InetAddress 类
    • Internet 上的主机有两种方式表示地址
      • 域名(hostName):www.baidu.com
      • IP 地址(hostAddress):183.232.231.172
    • InetAddress 类主要表示 IP 地址,有两个子类
      • Inet4Address
      • Inet6Address
    • InetAddress 类 对 象 含 有 一 个 Internet 主 机 地 址 的 域 名 和 IP 地 址(www.baidu.com 和 183.232.231.172)
    • 域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成 IP 地址,这样才能和主机建立连接 --> 域名解析
      • 先找本机 hosts(C:\Windows\System32\drivers\etc\hosts),是否有输入的域名地址,没有的话,再通过 DNS 服务器,找主机
    • InetAddress 类没有提供公共的构造器,而是提供静态方法来获取 InetAddress 实例
      构造器说明
      static InetAddress getLocalHost()直接获取拥有本地 IP 地址的 InetAddress 对象
      static InetAddress getByName(String host)参数可以为域名或者 IP
    • 方法
      方法说明
      String getHostAddress()返回 IP 地址字符串
      String getHostName()获取此 IP 地址的主机名
      boolean isReachable(int timeout)测试是否可以达到该地址
  5. TCP/IP 协议簇
    • 传输层协议中有两个非常重要的协议
      • 传输控制协议 TCP(Transmission Control Protocol)
      • 用户数据报协议 UDP(User Datagram Protocol)
    • TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议
    • IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信
    • TCP/IP 协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层、IP 层、传输层和应用层
  6. TCP 协议
    • 使用 TCP 协议前,须先建立 TCP 连接,形成传输数据通道
    • 传输前,采用“三次握手”方式,点对点通信,是可靠的
    • TCP 协议进行通信的两个应用进程:客户端、服务端
    • 在连接中可进行大数据量的传输
    • 传输完毕,需释放已建立的连接,效率低
    • 三次握手图示:
      在这里插入图片描述
    • 四次挥手图示:
      在这里插入图片描述
  7. UDP 协议
    • 将数据、源、目的封装成数据包,不需要建立连接
    • 每个数据报的大小限制在 64K 内
    • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
    • 可以广播发送
    • 发送数据结束时无需释放资源,开销小,速度快
  8. Socket
    • 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准

    • 网络上具有唯一标识的 IP 地址和端口号组合在一起才能构成唯一能识别的标识符套接字

    • 通信的两端都要有 Socket ,是两台机器间通信的端点

    • 网络通信其实就是 Socket 间的通信

    • Socket 允许程序把网络连接当成一个流,数据在两个 Socket 间通过 IO 传输

    • 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端

    • Socket 分类

      • 流套接字(stream socket):使用 TCP 提供可依赖的字节流服务
      • 数据报套接字(datagram socket):使用 UDP 提供“尽力而为”的数据报服务
    • 常用构造器和常用方法:

      构造器说明
      public Socket(InetAddress address,int port)创建一个流套接字并将其连接到指定 IP 地址的指定端口号
      public Socket(String host,int port)创建一个流套接字并将其连接到指定主机上的指定端口号
      方法说明
      InputStream getInputStream()返回此套接字的输入流,可以用于接收网络消息
      OutputStream getOutputStream()返回此套接字的输出流。可以用于发送网络消息
      InetAddress getInetAddress()此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null
      InetAddress getLocalAddress()获取套接字绑定的本地地址, 即本端的IP地址
      int getPort()此套接字连接到的远程端口号,如果尚未连接套接字,则返回 0
      int getLocalPort()返回此套接字绑定到的本地端口, 如果尚未绑定套接字,则返回 -1,即本端的端口号
      void close()关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定),需要创建新的套接字对象。,关闭此套接字也将会关闭该套接字的 InputStream 和OutputStream
      void shutdownInput()如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将返回 EOF(文件结束符), 即不能在从此套接字的输入流中接收任何数据
      void shutdownOutput()禁用此套接字的输出流,对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列, 如果在套接字上调用 shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException ,即不能通过此套接字的输出流发送任何数据(会在流末尾写入一个“流的末尾”标记,对方才能读到 -1,否则对方的读取方法会一直堵塞)

2.TCP 网络编程

  1. 通信模型
    在这里插入图片描述
  2. 服务器 ServerSocket 的工作过程
    • 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上,用于监听客户端的请求
    • 调用 accept():监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象
    • 调用该 Socket 类对象的 getOutputStream() 和 getInputStream ():获取输出流和输入流,开始网络数据的发送和接收
    • 关闭 ServerSocket 和 Socket 对象:客户端访问结束,关闭通信套接字
  3. 服务器建立 ServerSocket 对象
    • ServerSocket 对象负责等待客户端请求建立套接字连接,也就是说,服务器必须事先建立一个等待客户请求建立套接字连接的 ServerSocket 对象
    • 所谓“接收”客户的套接字请求,就是 accept() 方法会返回一个 Socket 对象
  4. 客户端 Socket 的工作过程
    • 创建 Socket:根据指定服务端的 IP 地址或端口号构造 Socket 类对象,若服务器端响应,则建立客户端到服务器的通信线路,若连接失败,会出现异常
    • 打开连接到 Socket 的输入/出流: 使用 getInputStream() 方法获得输入流,使用 getOutputStream() 方法获得输出流,进行数据传输
    • 按照一定的协议对 Socket 进行读/写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程
    • 关闭 Socket:断开客户端到服务器的连接,释放线路
  5. 客户端创建 Socket 对象
    • 客户端程序可以使用 Socket 类创建对象,创建的同时会自动向服务器方发起连接
      构造器说明
      Socket(String host,int port)throws UnknownHostException,IOException向服务器(域名是 host ,端口号为 port )发起 TCP 连接,若成功,则创建 Socket 对象,否则抛出异常
      Socket(InetAddress address,int port)throws IOException根据 InetAddress 对象所表示的 IP 地址以及端口号 port 发起连接
    • 客户端建立 socketAtClient 对象的过程就是向服务器发出套接字连接请求

3.UDP 网络编程

  1. UDP 网络通信

    • 类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序
    • UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP 数据报一定能够安全送到目的地,也不能确定什么时候可以抵达
    • DatagramPacket 对象封装了 UDP 数据报,在数据报中包含了发送端的 IP 地址和端口号以及接收端的 IP 地址和端口号
    • UDP 协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接,如同发快递包裹一样
  2. 步骤

    • DatagramSocket 与 DatagramPacket
    • 建立发送端,接收端
    • 建立数据包
    • 调用 Socket 的发送、接收方法
    • 关闭 Socket
  3. 发送端与接收端是两个独立的运行程序

  4. DatagramSocket 类

    构造器说明
    public DatagramSocket()一般由客户端创建,不需要指定 IP 和端口号,因为是在 DatagramPacket 中指定,该对象只负责发送
    public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口,套接字将被绑定到通配符地址,IP 地址由内核来选择
    public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址,本地端口必须在 0 到 65535 之间(包括两者),如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择
    方法说明
    void close()关闭此数据报套接字
    void send(DatagramPacket p)从此套接字发送数据报包,DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号
    void receive(DatagramPacket p)从此套接字接收数据报包,当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据,数据报包也包含发送方的 IP 地址和发送方机器上的端口号。,此方法在接收到数据报前一直阻塞,数据报包对象的 length 字段包含所接收信息的长度,如果信息比包的长度长,该信息将被截短
    InetAddress getLocalAddress()获取套接字绑定的本地地址
    int getLocalPort()返回此套接字绑定的本地主机上的端口号
    InetAddress getInetAddress()返回此套接字连接的地址,如果套接字未连接,则返回 null
    int getPort()返回此套接字的端口,如果套接字未连接,则返回 -1
  5. DatagramPacket 类

    构造器说明
    public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为 length 的数据包,length 参数必须小于等于 buf.length
    public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号,length 参数必须小于等于 buf.length
    方法说明
    InetAddress getAddress()返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的
    int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或者是从该主机接收到的
    byte[] getData()返回数据缓冲区,接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续 length 长度
    int getLength()返回将要发送或接收到的数据的长度
  6. 代码

    /**
    * 发送端
    */
    DatagramSocket ds = null;
    try {
    	ds = new DatagramSocket();
    	byte[] by = "hello,atguigu.com".getBytes();
    	DatagramPacket dp = new DatagramPacket(by, 0, by.length,
    	InetAddress.getByName("127.0.0.1"), 10000);
    	ds.send(dp);
    } catch (Exception e) {
    	e.printStackTrace();
    } finally {
    	if (ds != null){
    		ds.close();
    	}
    }
    
    
    /**
    * 接收端
    * 在接收端,要指定监听的端口
    */
    DatagramSocket ds = null;
    try {
    	ds = new DatagramSocket(10000);
    	byte[] by = new byte[1024];
    	DatagramPacket dp = new DatagramPacket(by, by.length);
    	ds.receive(dp);
    	String str = new String(dp.getData(), 0, dp.getLength());
    	System.out.println(str + "--" + dp.getAddress());
    } catch (Exception e) {
    	e.printStackTrace();
    } finally {
    	if (ds != null){
    		ds.close();
    	}
    }
    

4.URL 编程

  1. URL
    • URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址
    • 它是一种具体的 URI ,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源
    • 通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp 站点,浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源
    • URL 的基本结构由 5 部分组成:<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
      • 示例:http://192.168.1.100:8080/helloworld/index.jsp#?usernae=shkstart&password=123
      • #片段名:即锚点,例如看小说,直接定位到章节
      • 参数列表格式:参数名=参数值&参数名=参数值…
  2. URL 类
    • 为了表示URL,java.net 中实现了类 URL
    • URL 类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用 try-catch 语句进行捕获
      构造器说明示例
      public URL (String spec)通过一个表示 URL 地址的字符串可以构造一个 URL 对象URL url = new URL (“http://www. baidu.com/”);
      public URL(URL context, String spec)通过基 URL 和相对 URL 构造一个 URL 对象URL url= new URL(url, “download.html")
      public URL(String protocol, String host, String file)URL url = new URL(“http”,“www.baidu.com”, “download. html");
      public URL(String protocol, String host, int port, String file)URL url= new URL(“http”, “www.baidu.com”, 80, “download.html");
    • 一个 URL 对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性
      方法说明
      public String getProtocol()获取该 URL 的协议名
      public String getHost()获取该 URL 的主机名
      public String getPort()获取该 URL 的端口号
      public String getPath()获取该 URL 的文件路径
      public String getFile()获取该 URL 的文件名
      public String getQuery()获取该 URL 的查询名
  3. 针对 HTTP 协议的 URLConnection 类
    • URL 的方法 openStream():能从网络上读取数据
    • 若希望输出数据,例如向服务器端的 CGI (公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一些数据,则必须先与 URL 建立连接,然后才能对其进行读写,此时需要使用 URLConnection
    • URLConnection:表示到 URL 所引用的远程对象的连接,当与一个 URL 建立连接时,首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection 对象,如果连接过程失败,将产生 IOException
    • 通过 URLConnection 对象获取的输入流和输出流,即可以与现有的 CGI 程序进行交互
      方法
      InputStream getInputStream( ) throws IOException
      OutputSteram getOutputStream( ) throws IOException
      Object getContent( ) throws IOException
      int getContentLength( )
      String getContentType( )
      long getDate( )
      long getLastModified( )
  4. URI、URL 和 URN 的区别
    • URI(uniform resource identifier):统一资源标识符,用来唯一的标识一个资源
    • URL(uniform resource locator):统一资源定位符,它是一种具体的 URI ,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源
    • URN(uniform resource name):统一资源命名,是通过名字来标识资源,比如 mailto:java-net@java.sun.com
    • 总结
      • 也就是说,URI 是以一种抽象的,高层次概念定义统一资源标识,而 URL 和 URN 则是具体的资源标识的方式,URL 和 URN 都是一种 URI
      • 在 Java 的 URI 中,一个 URI 实例可以代表绝对的,也可以是相对的,只要它符合 URI 的语法规则
      • 而 URL 类则不仅符合语义,还包含了定位该资源的信息,因此它不能是相对的
  5. InetAddress 对象包含的两个字段:主机名(String) 和 IP 地址(int)

八 JDBC

资料
JDBC_1.pdf
JDBC_2.pdf
JDBC核心技术.exe
JDBC(下).pdf
JDBC接口.pdf

九 反射

资料
反射.pdf
反射(1).pdf

1.Java 反射机制概述

  1. Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法
  2. 加载完类之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象就包含了完整的类的结构信息,我们可以通过这个对象看到类的结构,这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射
  3. 关于 java.lang.Class 类的理解
    • 类的加载过程
      • 程序经过 javac.exe 命令以后,会生成一个或多个字节码文件(.class 结尾),接着我们使用 java.exe 命令对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,此过程就称为类的加载,加载到内存中的类,我们就称为运行时类,此运行时类,就作为 Class 的一个实例
    • 换句话说,Class 的实例就对应着一个运行时类
    • 加载到内存中的运行时类,会缓存一定的时间,在此时间之内,我们可以通过不同的方式来获取此运行时类
  4. Class 类的常用方法
    方法说明
    static Class forName(String name)返回指定类名 name 的 Class 对象
    T newInstance()调用缺省构造函数,返回该 Class 对象的一个实例
    String getName()返回此 Class 对象所表示的实体(类、接口、数组类、基本类型或 void)名称
    native Class<? super T> getSuperclass()返回当前 Class 对象的父类的 Class 对象
    Class<?>[ ] getInterfaces获取当前 Class 对象的接口
    ClassLoader getClassLoader()返回该类的类加载器
    native Class<? super T> getSuperclass返回表示此 Class 所表示的实体的超类的 Class
    Constructor<?>[ ] getConstructors()返回一个包含某些 Constructor 对象的数组
    Field[ ] getDeclaredFields()返回 Field 对象的一个数组
    Method getMethod(String name, Class<?>… parameterTypes)返回一个 Method 对象,此对象的形参类型为 paramType
  5. 获取 Class 类的实例
    • 方式一
      • 前提:若已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序性能最高
      • 示例:Class clazz = String.class;
    • 方式二
      • 前提:已知某个类的实例,调用该实例的 getClass() 方法获取 Class 对象
      • 示例:Class clazz = 对象名.getClass();
    • 方式三
      • 已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法 forName() 获取,可能抛出 ClassNotFoundException
      • 示例:Class clazz = Class.forName(“java.lang.String”);
    • 方式四
      • 使用类的加载器:ClassLoader
      • 示例:Class clazz=this.getClass().getClassLoader().loadClass(“类的全类名”);
  6. 可以有 Class 对象的类型
    • class
    • interface
    • enum
    • annotation
    • 数组(只要数组的元素类型与维度一样,就是同一个 Class)
    • 基本数据类型
    • void

2.类加载器 ClassLoader

  1. 类的加载过程
    • 类的加载:将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象,此过程由类加载器完成
      • 将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口(即引用地址),所有需要访问和使用类数据只能通过这个 Class 对象,这个加载的过程需要类加载器参与
    • 类的链接
      • 验证:确保加载的类信息符合 JVM 规范,例如:以 cafe 开头,没有安全方面的问题
      • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配
      • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
    • 类的初始化
      • 执行类构造器 () 方法的过程,类构造器 () 方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的(类构造器是构造类信息的,不是构造该类对象的构造器)
      • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
      • 虚拟机会保证一个类的 () 方法在多线程环境中被正确加锁和同步
  2. 什么时候会发生类初始化
    • 类的主动引用(一定会发生类的初始化)
      • 当虚拟机启动,先初始化 main() 方法所在的类
      • new 一个类的对象
      • 调用类的静态成员(除了 final 常量)和静态方法
      • 使用 java.lang.reflect 包的方法对类进行反射调用
      • 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
    • 类的被动引用(不会发生类的初始化)
      • 当访问一个静态域时,只有真正声明这个域的类才会被初始化(当通过子类引用父类的静态变量,不会导致子类初始化)
      • 通过数组定义类引用,不会触发此类的初始化
      • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
    • 示例
      public class ClassLoadingTest {
      	static {
      		System.out.println("main所在的类");
      	}
      
      	public static void main(String[] args) {
      		// 主动引用:一定会导致 A 和 Father 的初始化
      		// A a = new A();
      		// System.out.println(A.m);
      		// Class.forName("com.A");
      		
      		// 被动引用
      		A[] array = new A[5];	// 不会导致 A 和 Father 的初始化
      		// System.out.println(A.b);// 只会初始化 Father
      		// System.out.println(A.M);// 不会导致 A 和 Father 的初始化
      	}
      }
      
      
      class A extends Father {
      	static {
      		System.out.println("子类被加载");
      		m = 300;
      	}
      	
      	static int m = 100;
      	static final int M = 1;
      }
      
      
      class Father {
      	static {
      		System.out.println("父类被加载");
      	}
      	
      	static int b = 2;
      }
      
  3. 类加载器的作用
    • 类加载的作用:将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区中类数据的访问入口
    • 类缓存:标准的 JavaSE 类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间,不过 JVM 垃圾回收机制可以回收这些 Class 对象
    • 图示
      在这里插入图片描述
  4. 类加载器的类型
    • 引导类加载器:用 C++ 编写的,是 JVM 自带的类加载器,负责 Java 平台核心库,用来装载核心类库,该加载器无法直接获取
    • 扩展类加载器:负责 jre/lib/ext 目录下的 jar 包或 –D java.ext.dirs 指定目录下的 jar 包装入工作库
    • 系统类加载器:负责 java –classpath 或 –D java.class.path 所指的目录下的类与 jar 包装入工作 ,是最常用的加载器
    • 示例
      // 1.获取一个系统类加载器
      ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
      // sun.misc.Launcher$AppClassLoader@18b4aac2
      System.out.println(systemClassLoader);
      
      // 2.获取系统类加载器的父类加载器,即扩展类加载器
      ClassLoader extensionClassLoader = systemClassLoader.getParent();
      // sun.misc.Launcher$ExtClassLoader@14ae5a5
      System.out.println(extensionClassLoader);
      
      //3.获取扩展类加载器的父类加载器,即引导类加载器
      ClassLoader bootClassLoader = extensionClassLoader.getParent();
      // null
      System.out.println(bootClassLoader);
      
      //4.测试当前类由哪个类加载器进行加载
      ClassLoader currentClassLoader = Class.forName("test.Test").getClassLoader();
      System.out.println(currentClassLoader);
      
      //5.测试JDK提供的Object类由哪个类加载器加载
      bootClassLoader =Class.forName("java.lang.Object").getClassLoader();
      System.out.println(bootClassLoader );
      
      // 6.关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流
      public void test() throws IOException {
      	Properties pros =  new Properties();
      	
      	// 此时的文件默认在当前的 module 下
      	// 读取配置文件的方式一:
      	// FileInputStream fis = new FileInputStream("jdbc.properties");
      	// pros.load(fis);
      	
      	// 配置文件默认识别为:当前 module 的 src 下
      	// 读取配置文件的方式二:使用 ClassLoader
      	ClassLoader classLoader = Test.class.getClassLoader();
      	InputStream is = classLoader.getResourceAsStream("jdbc.properties");
      	pros.load(is);
      	
      	String user = pros.getProperty("user");
      	String password = pros.getProperty("password");
      	System.out.println("user = " + user + ",password = " + password);
      }
      
    • 图示
      在这里插入图片描述

3.创建对象与获取完整结构

  1. 创建运行时类的对象

    • newInstance():调用此方法,创建对应的运行时类的对象,内部调用了运行时类的空参的构造器
    • 要想此方法正常的创建运行时类的对象,要求:
      • 运行时类必须提供空参的构造器
      • 空参的构造器的访问权限得够。通常,设置为public
    • 在 javabean 中要求提供一个 public 的空参构造器,原因:
      • 便于通过反射,创建运行时类的对象
      • 便于子类继承此运行时类时,默认调用 super() 时,保证父类有此构造器
  2. 使用有参构造器创建对象

    • 通过 Class 类的 getDeclaredConstructor(Class … parameterTypes) 取得本类的指定形参类型的构造器
    • 向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数
    • 通过 Constructor 实例化对象
    • 示例
      // 1.根据全类名获取对应的Class对象
      Class clazz = Class.forName("pojo.Person");
      
      // 2.调用指定参数结构的构造器,生成Constructor的实例
      Constructor con = clazz.getConstructor(String.class,Integer.class);
      
      // 3.通过Constructor的实例创建对应类的对象,并初始化类属性
      Person p2 = (Person) con.newInstance("Peter",20);
      System.out.println(p2);
      
  3. 获取运行时类的完整结构

    获取属性(字段)的方法说明
    Field[ ] getFields()获取当前运行时类及其父类中声明为 public 访问权限的属性
    Field[ ] getDeclaredFields()获取当前运行时类中声明的所有属性(不包含父类中声明的属性)
    int getModifiers()获取属性的权限修饰符
    Class<?> getType()获取属性的类型
    String getName()获取属性名
    获取方法的方法说明
    Method[ ] getMethods()获取当前运行时类及其所有父类中声明为 public 权限的方法
    Method[ ] getDeclaredMethods()获取当前运行时类中声明的所有方法(不包含父类中声明的方法)
    Annotation[ ] getAnnotations()获取方法声明的注解
    int getModifiers()获取方法的权限修饰符
    Class<?> getReturnType()获取方法的返回值类型
    String getName()获取方法的方法名
    Class<?>[ ] getParameterTypes()获取方法的形参列表
    Class<?>[ ] getExceptionTypes()获取方法的抛出的异常的类型
    获取构造器的方法说明
    Constructor<?>[ ] getConstructors()获取当前运行时类中声明为 public 的构造器
    Constructor<?>[ ] getDeclaredConstructors()获取当前运行时类中声明的所有的构造器
    int getModifiers()获取构造器的权限修饰符
    String getName()获取构造器的名称
    Class<?>[] getParameterTypes()获取构造器的参数的类型
    获取父类的方法说明
    native Class<? super T> getSuperclass()获取运行时类的父类
    Type getGenericSuperclass()获取运行时类的带泛型的父类的泛型(需要强转为 ParameterizedType,才有 getActualTypeArguments 方法)
    Type[ ] getActualTypeArguments()获取泛型类型
    获取接口的方法说明
    Class<?>[ ] getInterfaces()获取运行时类实现的所有接口
    获取注解的方法说明
    Annotation[] getAnnotations()获取运行时类声明的注解
    获取运行类所在包的方法说明
    Package getPackage()获取运行类所在类的包
  4. 调用指定方法

    • 通过 Class 类的 getMethod(String name,Class…parameterTypes) 方法取得一个 Method 对象,并设置此方法操作时所需要的参数类型
    • 之后使用 Object invoke(Object obj, Object[] args) 进行调用,并向方法中传递要设置的 obj 对象的参数信息
  5. 调用指定属性

    • Field getField(String name):返回此 Class 对象表示的类或接口的指定的 public 的 Field
    • Field getDeclaredField(String name):返回此 Class 对象表示的类或接口的指定的 Field
    • 在 Field 中
      • Object get(Object obj):取得指定对象 obj 上此 Field 的属性内容
      • void set(Object obj,Object value) 设置指定对象 obj 上此 Field 的属性内容
  6. 关于 Object invoke(Object obj, Object … args)

    • Object 对应原方法的返回值,若原方法无返回值,此时返回 null
    • 若原方法若为静态方法,此时形参 Object obj 可为 null
    • 若原方法形参列表为空,则 Object[] args 为 null
    • 若原方法声明为 private,则需要在调用此 invoke() 方法前,显式调用方法对象的 setAccessible(true) 方法,将可访问 private 的方法
  7. 关于 setAccessible()

    • Method 和 Field、Constructo r对象都有 setAccessible() 方法
    • setAccessible() 启动和禁用访问安全检查的开关
    • 参数值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查
      • 提高反射的效率:如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为 true
      • 使得原本无法访问的私有成员也可以访问
    • 参数值为 false 则指示反射的对象应该实施 Java 语言访问检查

4.动态代理

  1. 相关 API
    • Proxy 类:专门完成代理的操作类,是所有动态代理类的父类,通过此类为一个或多个接口动态地生成实现类
    • 提供用于创建动态代理类和动态代理对象的静态方法
      • static Class<?> getProxyClass(ClassLoader loader, Class<?>… interfaces):创建一个动态代理类所对应的 Class 对象
      • static Object newProxyInstance(ClassLoader loader, Class<?>[ ] interfaces,InvocationHandler h):直接创建一个动态代理对象
        • loader:类加载器
        • interfaces:得到被代理类实现的全部接口
        • h:得到 InvocationHandler 接口的实现类实例
  2. 动态代理示例
    public class ProxyTest {
    
        public static void main(String[] args) {
            SuperMan superMan = new SuperMan();
            
            // proxyInstance:代理类的对象
            Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
            
            // 当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
            String belief = proxyInstance.getBelief();
            System.out.println(belief);
            proxyInstance.eat("四川麻辣烫");
        }
    }
    
    
    interface Human{
        String getBelief();
        
        void eat(String food);
    }
    
    
    // 被代理类
    class SuperMan implements Human {
        @Override
        public String getBelief() {
            return "I believe I can fly!";
        }
    
        @Override
        public void eat(String food) {
            System.out.println("我喜欢吃" + food);
        }
    }
    
    /**
     * 要想实现动态代理,需要解决的问题?
     * 问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象
     * 问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a
     */
    class ProxyFactory{
        // 调用此方法,返回一个代理类的对象,解决问题一
        public static Object getProxyInstance(Object obj){	// obj:被代理类的对象
            MyInvocationHandler handler = new MyInvocationHandler();
            // 此方法为自定义的,并不是官方的方法
            handler.bind(obj);
    
            return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),handler);
        }
    }
    
    
    class MyInvocationHandler implements InvocationHandler{
        private Object obj;	// 需要使用被代理类的对象进行赋值
    
        public void bind(Object obj){
            this.obj = obj;
        }
    
        // 当我们通过代理类的对象,调用方法 a 时,就会自动的调用如下的方法:invoke()
        // 将被代理类要执行的方法 a 的功能就声明在 invoke() 中
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        	// 模拟加上去的通用方法,并不是必须
        	HumanUtil util = new HumanUtil();
        	util.method1();
    		// System.out.println("通用方法一");
    		
            //method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
            //obj:被代理类的对象
            Object returnValue = method.invoke(obj,args);
    
    		util.method2();
            // System.out.println("通用方法二");
    
            //上述方法的返回值就作为当前类中的invoke()的返回值。
            return returnValue;
        }
    }
    
    
    /**
     * 这里是通用方法(可以不需要)
     */
    class HumanUtil{
        public void method1(){
            System.out.println("====================通用方法一====================");
    
        }
    
        public void method2(){
            System.out.println("====================通用方法二====================");
        }
    }
    
  3. 动态代理与 AOP(Aspect Orient Programming)
    • 使用 Proxy 生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的意义,通常都是为指定的目标对象生成动态代理
    • 这种动态代理在 AOP 中被称为 AOP 代理,AOP 代理可代替目标对象,AOP 代理包含了目标对象的全部方法
    • 但 AOP 代理中的方法与目标对象的方法存在差异:AOP 代理里的方法可以在执行目标方法之前、之后插入一些通用处理

十 枚举类与注解

1.枚举类的使用

  1. 枚举类的使用
    • 枚举类的理解:类的对象只有有限个,确定的,我们称此类为枚举类
    • 当需要定义一组常量时,强烈建议使用枚举类
    • 如果枚举类中只有一个对象,则可以作为单例模式的实现方式
  2. 如何定义枚举类
    • 方式一:JDK1.5 之前,自定义枚举类
    • 方式二:JDK1.5,可以使用 enum 关键字定义枚举类
  3. 自定义枚举类
    1. 步骤

      • 声明枚举类对象的属性:private final 修饰(也可没有)
      • 私有化类的构造器(并给对象属性赋值)
      • 提供当前枚举类的多个对象:public static final 修饰
      • 其他诉求1:获取枚举类对象的属性
      • 其他诉求2:提供 toString()
    2. 枚举类的属性

      • 枚举类对象的属性不应允许被改动,所以应该使用 private final 修饰
      • 枚举类的使用 private final 修饰的属性应该在构造器中为其赋值
      • 若枚举类显式的定义了带参数的构造器,则在列出枚举值时也必须对应的传入参数
    3. 示例

      class Season{
      	private final String SEASONNAME;//季节的名称
      	private final String SEASONDESC;//季节的描述
      	
      	private Season(String seasonName,String seasonDesc){
      		this.SEASONNAME = seasonName;
      		this.SEASONDESC = seasonDesc;
      	}
      	
      	public static final Season SPRING = new Season("春天", "春暖花开");
      	public static final Season SUMMER = new Season("夏天", "夏日炎炎");
      	public static final Season AUTUMN = new Season("秋天", "秋高气爽");
      	public static final Season WINTER = new Season("冬天", "白雪皑皑");
      	
      	/**
      	 * 后面为其他诉求
      	 */
      	public String getSeasonName() {
      		return seasonName;
      	}
      	public String getSeasonDesc() {
      		return seasonDesc;
      	}
      	@Override
      	public String toString() {
      		return "Season{" +
      				"seasonName='" + seasonName + '\'' +
      				", seasonDesc='" + seasonDesc + '\'' +
      				'}';
      	}
      }
      
  4. 使用 enum 定义枚举类
    1. 步骤
      • 提供当前枚举类的对象,多个对象之间用 “,” 隔开,末尾对象 “;” 结束
      • 声明 Season 对象的属性:private final 修饰
      • 私有化类的构造器,并给对象属性赋值
      • 其他诉求1:获取枚举类对象的属性
      • 其他诉求2:提供 toString()
    2. 使用说明
      • 使用 enum 定义的枚举类默认继承了 java.lang.Enum 类,因此不能再继承其他类
      • 枚举类的构造器只能使用 private 权限修饰符
      • 枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾),列出的实例系统会自动添加 public static final 修饰
      • 必须在枚举类的第一行声明枚举类对象
    3. JDK 1.5 中可以在 switch 表达式中使用 Enum 定义的枚举类的对象作为表达式,case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定
    4. 示例
      public enum SeasonEnum {
      	SPRING("春天","春风又绿江南岸"),
      	SUMMER("夏天","映日荷花别样红"),
      	AUTUMN("秋天","秋水共长天一色"),
      	WINTER("冬天","窗含西岭千秋雪");
      	
      	private final String seasonName;
      	private final String seasonDesc;
      	
      	private SeasonEnum(String seasonName, String seasonDesc) {
      		this.seasonName = seasonName;
      		this.seasonDesc = seasonDesc;
      	}
      	
      	public String getSeasonName() {
      		return seasonName;
      	}
      	public String getSeasonDesc() {
      		return seasonDesc;
      	}
      }
      
  5. Enum 类的主要方法
    方法说明
    values()返回枚举类型的对象数组,该方法可以很方便地遍历所有的枚举值
    valueOf(String str)可以把一个字符串转为对应的枚举类对象,要求字符串必须是枚举类对象的“名字”,如不是,会有运行时异常:IllegalArgumentException
    String toString()返回当前枚举类对象常量的名称
    final boolean equals(Object other)在枚举类型中可以直接使用“==”来比较两个枚举类常量是否相等,Enum 提供的这个 equals() 方法,也是直接使用“==”实现的,它的存在是为了在 Set、List 和 Map 中使用,注意,equals() 是不可变的
    final int hashCode()Enum 实现了 hashCode() 来和 equals() 保持一致,它也是不可变的
    final Class getDeclaringClass()得到枚举类常量所属枚举类型的 Class 对象,可以用它来判断两个枚举类常量是否属于同一个枚举类型
    final String name()得到当前枚举常量的名称,建议优先使用 toString()
    final int ordinal得到当前枚举常量的次序
    final int compareTo(E o)枚举类型实现了 Comparable 接口,这样可以比较两个枚举常量的大小(按照声明的顺序排列)
    final Object clone()枚举类不能被 clone,为了防止子类实现克隆方法,Enum 实现了一个仅抛出 CloneNotSupportedException 异常的不变 clone()
  6. 枚举类实现接口
    • 和普通 Java 类一样,枚举类可以实现一个或多个接口
    • 示例
      interface Info{
          void show();
      }
      
      public enum Season implements Info {
      	/**
           *若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式, 
           *则可以让每个枚举值分别来实现该方法
           */
          SPRING("春天","春暖花开"){
              @Override
              public void show() {
                  System.out.println("春天 show()");
              }
          },
          SUMMER("夏天","夏日炎炎"){
              @Override
              public void show() {
                  System.out.println("夏天 show()");
              }
          },
          AUTUMN("秋天","秋高气爽"){
              @Override
              public void show() {
                  System.out.println("秋天 show()");
              }
          },
          WINTER("冬天","冰天雪地"){
              @Override
              public void show() {
                  System.out.println("冬天 show()");
              }
          };
      
          private final String seasonName;
          private final String seasonDesc;
      
          private Season(String seasonName,String seasonDesc){
              this.seasonName = seasonName;
              this.seasonDesc = seasonDesc;
          }
      
          public String getSeasonName() {
              return seasonName;
          }
      
          public String getSeasonDesc() {
              return seasonDesc;
          }
      	
      	/**
           * 若每个枚举值在调用实现的接口方法呈现相同的行为方式,
           * 则只要统一实现该方法即可
           */
      	/*
          @Override
          public void show() {
              System.out.println("这是一个季节");
          }
          */
      }
      

2.注解

  1. 注解的概述
    • 从 JDK1.5 开始,Java 增加了对元数据(MetaData) 的支持, 也就是 Annotation(注解)
    • 注解其实就是代码里的特殊标记,这些标记可以在编译、类加载、 运行时被读取, 并执行相应的处理,通过使用注解, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息,代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署
    • 注解可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被保存在 Annotation 的 “name=value” 对中
    • 在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等,在 JavaEE/Android 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 JavaEE 旧版中所遗留的繁冗代码和 XML 配置等
    • 未来的开发模式都是基于注解的,JPA 是基于注解的,Spring2.5 以上都是基于注解的,Hibernate3.x 以后也是基于注解的,现在的 Struts2 有一部分也是基于注解的了,注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式
  2. 注解的使用示例
    • 生成文档相关的注解
    • 在编译时进行格式检查(JDK 内置的三个基本注解)
      • @Override:限定重写父类方法,该注解只能用于方法
      • @Deprecated:用于表示所修饰的元素(类, 方法等)已过时,通常是因为所修饰的结构危险或存在更好的选择
      • @SuppressWarnings:抑制编译器警告
    • 跟踪代码依赖性,实现替代配置文件功能
      • 示例
        /**
         * Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署
         */
        @WebServlet("/login")
        public class LoginServlet extends HttpServlet {
        	private static final long serialVersionUID = 1L;
        	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { }
        	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        		doGet(request, response);
        	}
        }
        
        <servlet>
        	<servlet-name>LoginServlet</servlet-name>
        	<servlet-class>com.servlet.LoginServlet</servlet-class>
        </servlet>
        <servlet-mapping>
        	<servlet-name>LoginServlet</servlet-name>
        	<url-pattern>/login</url-pattern>
        </servlet-mapping>
        
        
        /**
         *  spring框架中关于“事务”的管理
         */
        @Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED,readOnly=false,timeout=3)
        public void buyBook(String username, String isbn) {
        	// 1.查询书的单价
        	int price = bookShopDao.findBookPriceByIsbn(isbn);
        	// 2. 更新库存
        	bookShopDao.updateBookStock(isbn);
        	// 3. 更新用户的余额
        	bookShopDao.updateUserAccount(username, price);
        }
        
        <!-- 配置事务属性 -->
        <tx:advice transaction-manager="dataSourceTransactionManager" id="txAdvice">
        	<tx:attributes>
        	<!-- 配置每个方法使用的事务属性 -->
        	<tx:method name="buyBook" propagation="REQUIRES_NEW"
        		isolation="READ_COMMITTED" read-only="false" timeout="3" />
        	</tx:attributes>
        </tx:advice>
        
  3. 自定义注解
    1. 概述
      • 定义新的注解类型使用 @interface 关键字
      • 自定义注解自动继承了 java.lang.annotation.Annotation 接口
      • 注解的成员变量在注解定义中以无参数方法的形式来声明,其方法名和返回值定义了该成员的名字和类型,我们称为配置参数,类型只能是八种基本数据类型、String 类型、Class 类型、enum 类型、Annotation 类型、及以上所有类型的数组
      • 可以在定义注解的成员变量时为其指定初始值,指定成员变量的初始值可使用 default 关键字
      • 如果只有一个参数成员,建议使用参数名为 value
      • 如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值,格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为 value,可以省略 “value=”
      • 没有成员定义的 Annotation 称为标记,包含成员变量的 Annotation 称为元数据 Annotation
      • 注意:自定义注解必须配上注解的信息处理流程才有意义
    2. 步骤(参照 @SuppressWarnings 定义)
      • 注解声明为:@interface
      • 内部定义成员,通常使用 value 表示
      • 可以指定成员的默认值,使用 default 定义
    3. 如果自定义注解没有成员,表明是一个标识作用
    4. 如果注解有成员,在使用注解时,需要指明成员的值
    5. 自定义注解通过都会指明两个元注解:Retention、Target
  4. JDK 中提供的 4 种元注解
    1. 元注解:对现有的注解进行解释说明的注解
    2. @Retention:用于指定该注解的生命周期,包含一个 RetentionPolicy 类型的成员变量,使用时必须为该 value 成员变量指定值
      元注解可取值说明
      RetentionPolicy.SOURCE在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释
      RetentionPolicy.CLASS在 class 文件中有效(即 class 保留),当运行 Java 程序时, JVM 不会保留注解, 这是默认值
      RetentionPolicy.RUNTIME在运行时有效(即运行时保留),当运行 Java 程序时, JVM 会保留注释,程序可以通过反射获取该注释
    3. @Target:用于指定被修饰的注解能用于修饰哪些程序元素, 包含一个名为 value 的成员变量
      元注解可取值(ElementType)说明
      LOCAL_VARIABLE用于描述局部变量
      METHOD用于描述方法
      CONSTRUCTOR用于描述构造器
      FIELD用于描述域
      TYPE用于描述类、接口(包括注解类型)或 enum 声明
      PACKAGE用于描述包
      PARAMETER用于描述参数
    4. @Documented:用于指定被该元注解修饰的注解类将被 javadoc 工具提取成文档,默认情况下,javadoc 是不包括注解的
      • 定义为 Documented 的注解必须设置 Retention 值为 RUNTIME
    5. @Inherited:被它修饰的注解将具有继承性,如果某个类使用了被该元注解修饰的注解, 则其子类将自动具有该注解
      • 如果把标有该元注解的自定义的注解标注在类级别上,子类则可以继承父类类级别的注解
      • 实际应用中,使用较少
  5. 利用反射获取注解信息
    • JDK1.5 在 java.lang.reflect 包下新增了 AnnotatedElement 接口, 该接口代表程序中可以接受注解的程序元素
    • 当一个注解类型被定义为运行时注解后,该注解才是运行时可见,当 class 文件被载入时保存在 class 文件中的注解才会被虚拟机读取
    • 程序可以调用 AnnotatedElement 对象的如下方法来访问注解信息
      方法
      <T extends Annotation> T getAnnotation(Class<T> annotationClass)
      Annotation[ ] getAnnotations()
      Annotation[ ] getDeclaredAnnotations()
      default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
  6. JDK1.8 中注解的新特性
    • 此外,反射也得到了加强,在 Java1.8 中能够得到方法参数的名称,这会简化标注在方法参数上的注解
    • 可重复的注解
      // JDK1.8 之前的写法
      // @MyAnnotations({@MyAnnotation(value="h"),@MyAnnotation(value="hh")})
      // JDK1.8 之后的写法
      @MyAnnotation(value="hi")
      @MyAnnotation(value="abc")
      class Person{
      
      }
      
      
      @Inherited
      // JDK1.8 以后靠这个
      @Repeatable(MyAnnotations.class)
      @Retention(RetentionPolicy.RUNTIME)
      @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE,TYPE_PARAMETER,TYPE_USE})
      public @interface MyAnnotation {
      
          String value() default "hello";
      }
      
      
      @Inherited
      @Retention(RetentionPolicy.RUNTIME)
      @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
      public @interface MyAnnotations {
      	
      	// JDK1.8 以前靠这个
          MyAnnotation[] value();
      }
      
    • 类型注解
      • JDK1.8 之后,关于元注解 @Target 的参数类型 ElementType 枚举值多了两个:TYPE_PARAMETER 和 TYPE_USE
      • 在 Java1.8 之前,注解只能是在声明的地方所使用,Java1.8 开始,注解可以应用在任何地方
        • ElementType.TYPE_PARAMETER:表示该注解能写在类型变量的声明语句中(如:泛型声明)
        • ElementType.TYPE_USE:表示该注解能写在使用类型的任何语句中
      • 示例
        /**
         * 测试 ElementType.TYPE_PARAMETER
         */
        @Target({ElementType.TYPE_PARAMETER})
        @interface TypeDefine{
        
        }
        
        public class TestTypeDefine<@TypeDefine() U> {
        	private U u;
        	
        	public <@TypeDefine() T> void test(T t){
        
        	}
        }
        
        
        /**
         * 测试 ElementType.TYPE_USE
         */
        @Target(ElementType.TYPE_USE)
        @interface MyAnnotation {
        
        }
        
        @MyAnnotation
        public class AnnotationTest<U> {
        	@MyAnnotation
        	private String name;
        	
        	public static void main(String[] args) {
        		AnnotationTest<@MyAnnotation String> t = null;
        		int a = (@MyAnnotation int) 2L;
        		@MyAnnotation
        		int b = 10;
        	}
        	
        	public static <@MyAnnotation T> void method(T t) {
        	
        	}
        	
        	public static void test(@MyAnnotation String arg) throws @MyAnnotation Exception {
        	
        	}
        }
        

十一 Java8 的其它新特性

资料
第16章_Java8 的其它新特性.pdf
Java8 新特性.bmp
Java8 的新并行 API.pdf

1.Java8 新特性简介

  1. 图示
    在这里插入图片描述
  2. 特点
    • 速度更快
    • 代码更少(增加了新的语法:Lambda 表达式)
    • 强大的 Stream API
    • 便于并行
    • 最大化减少空指针异常:Optional
    • Nashorn 引擎,允许在 JVM 上运行 JS 应用
  3. 并行流与串行流
    • 并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流,相比较串行的流,并行的流可以很大程度上提高程序的执行效率
    • Java8 中将并行进行了优化,我们可以很容易的对数据进行并行操作
    • Stream API 可以声明性地通过 parallel() 与 sequential() 在并行流与顺序流之间进行切换

2.Lambda 表达式

  1. 为什么使用 Lambda 表达式
    • Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递),使用它可以写出更简洁、更灵活的代码,作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升
  2. 语法
    • 在Java8 语言中引入的一种新的语法元素和操作符,这个操作符为 “->” , 该操作符被称为 Lambda 操作符或箭头操作符,它将 Lambda 分为两个部分:
      • 左侧:指定了 Lambda 表达式需要的参数列表
      • 右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即 Lambda 表达式要执行的功能(其实就是重写的抽象方法的方法体)
    • 语法格式一:无参,无返回值
      Runnable r1=()->{System.out.println("Hello Lambdal!");};
      
    • 语法格式二:Lambda 需要一个参数,但是没有返回值
      Consumer<String> con=(String str)->{System.out.println(str);};
      
    • 语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
      Consumer<String> con=(str)->{System.out.println(str);};
      
    • 语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略
      Consumer<String> con=str->{System.out.println(str);};
      
    • 语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
      Comparator<Integer> com=(x,y)->{
      	System.out.println("实现函数式接口方法!");
      	return Integer.compare(x,y);
      };
      
    • 语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
      Comparator<Integer> com=(x,y)->Integer.compara(x,y);
      
  3. 语法总结
    • 左边:lambda 形参列表的参数类型可以省略(类型推断),如果 lambda 形参列表只有一个参数,其一对 () 也可以省略
    • 右边:lambda 体应该使用一对 { } 包裹,如果 lambda 体只有一条执行语句(可能是 return 语句),省略这一对 { } 和 return 关键字
  4. Lambda 表达式的本质:作为函数式接口的实例
  5. 如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口,我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口
  6. 以前用匿名实现类表示的现在都可以用Lambda表达式来写
  7. 类型推断
    • Lambda 表达式中的参数类型都是由编译器推断得出的,Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型,Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的,这就是所谓的“类型推断”

3.函数式(Functional)接口

  1. 什么是函数式接口
    • 只包含一个抽象方法的接口,称为函数式接口
    • 可以通过 Lambda 表达式来创建该接口的对象(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)
    • 可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口
    • 在 java.util.function 包下定义了Java8 的丰富的函数式接口
  2. 如何理解函数式接口
    • Java 从诞生日起就是一直倡导“一切皆对象”,在 Java 里面面向对象(OOP)编程是一切,但是随着 python、scala 等语言的兴起和新技术的挑战,Java 不得不做出调整以便支持更加广泛的技术要求,也即 java 不但可以支持 OOP 还可以支持 OOF(面向函数编程)
    • 在函数式编程语言当中,函数被当做一等公民对待,在将函数作为一等公民的编程语言中,Lambda 表达式的类型是函数,但是在 Java8 中,有所不同,在 Java8 中,Lambda 表达式是对象,而不是函数,它们必须依附于一类特别的对象类型——函数式接口
    • 简单的说,在 Java8 中,Lambda 表达式就是一个函数式接口的实例,这就是 Lambda 表达式和函数式接口的关系,也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用 Lambda 表达式来表示
    • 所以以前用匿名实现类表示的现在都可以用 Lambda 表达式来写
  3. Java 内置四大核心函数式接口
    函数式接口参数类型返回类型用途
    Consumer<T>
    消费型接口
    Tvoid对类型为 T 的对象应用操作,包含方法:void accept(T t)
    Supplier<T>
    供给型接口
    T返回类型为 T 的对象,包含方法:T get()
    Function<T, R>
    函数型接口
    TR对类型为 T 的对象应用操作,并返回结果,结果是 R 类型的对象,包含方法:R apply(T t)
    Predicate<T>
    断定型接口
    Tboolean确定类型为 T 的对象是否满足某约束,并返回 boolean 值,包含方法:boolean test(T t)
  4. 其他接口
    函数式接口参数类型返回类型用途
    BiFunction<T, U, R>T, UR对类型为 T, U 参数应用操作,返回 R 类型的结果,包含方法为: R apply(T t, U u)
    UnaryOperator<T>
    (Function子接口)
    TT对类型为 T 的对象进行一元运算,并返回 T 类型的结果,包含方法为:T apply(T t)
    BinaryOperator<T>
    (BiFunction 子接口)
    T, TT对类型为 T 的对象进行二元运算,并返回 T 类型的结果,包含方法为: T apply(T t1, T t2);
    BiConsumer<T, U>T, Uvoid对类型为 T, U 参数应用操作,包含方法为: void accept(T t, U u)
    BiPredicate<T,U>T,Uboolean包含方法为: boolean test(T t,U u)
    ToIntFunction<T>
    ToLongFunction<T>
    ToDoubleFunction<T>
    Tint
    long
    double
    分别计算 int、long、double 值的函数
    IntFunction<R>
    LongFunction<R>
    DoubleFunction<R>
    int
    long
    double
    R参数分别为 int、long、double 类型的函数

4.方法引用与构造器引用

  1. 方法引用(Method References)
    1. 使用情境:当要传递给 Lambda 体的操作,已经有实现的方法了,可以使用方法引用
    2. 方法引用可以看做是 Lambda 表达式深层次的表达,换句话说,方法引用就是 Lambda 表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是 Lambda 表达式的一个语法糖
    3. 使用格式:类(或对象) :: 方法名
    4. 具体分为如下的三种情况
      • 对象 :: 非静态方法
      • 对象 :: 非静态方法
      • 类 :: 非静态方法
    5. 要求
      • 要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同(针对于前两种情况)
    6. 示例
      • 情况一:对象 :: 实例方法
        /**
         * Consumer 中的 void accept(T t)
         * PrintStream 中的 void println(T t)
         */
        @Test
        public void test() {
        	Consumer<String> con1 = str -> System.out.println(str);
        	con1.accept("test");
        
        	System.out.println("*******************");
        	
        	PrintStream ps = System.out;
        	Consumer<String> con2 = ps::println;
        	con2.accept("test");
        }
        
        
        /**
         * Supplier 中的 T get()
         * Employee 中的 String getName()
         */
        @Test
        public void test() {
        	Employee emp = new Employee(1001,"Tom");
        
        	Supplier<String> sup1 = () -> emp.getName();
        	System.out.println(sup1.get());
        
        	System.out.println("*******************");
        	
        	Supplier<String> sup2 = emp::getName;
        	System.out.println(sup2.get());
        
        }
        
      • 情况二:类 :: 静态方法
        /**
         * Comparator 中的 int compare(T t1,T t2)
         * Integer 中的 int compare(T t1,T t2)
         */
         @Test
         public void test() {
        	Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2);
        	System.out.println(com1.compare(12,21));
        
        	System.out.println("*******************");
        
        	Comparator<Integer> com2 = Integer::compare;
        	System.out.println(com2.compare(12,21));
        }
        
        
        /**
         * Function 中的 R apply(T t)
         * Math 中的 Long round(Double d)
         */
        @Test
        public void test() {
        	Function<Double,Long> func = new Function<Double, Long>() {
        		@Override
        		public Long apply(Double d) {
        			return Math.round(d);
        		}
        	};
        
        	System.out.println("*******************");
        
        	Function<Double,Long> func1 = d -> Math.round(d);
        	System.out.println(func1.apply(12.3));
        
        	System.out.println("*******************");
        
        	Function<Double,Long> func2 = Math::round;
        	System.out.println(func2.apply(12.3));
        }
        
      • 情况三:类 :: 实例方法(当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时:ClassName::methodName)(有难度)
        /**
         * Comparator 中的 int comapre(T t1,T t2)
         * String 中的 int t1.compareTo(t2)
         */
        @Test
        public void test() {
        	Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2);
        	System.out.println(com1.compare("abc","abd"));
        
        	System.out.println("*******************");
        
        	Comparator<String> com2 = String :: compareTo;
        	System.out.println(com2.compare("abc","abd"));
        }
        
        
        /**
         * BiPredicate 中的 boolean test(T t1, T t2)
         * String 中的 boolean t1.equals(t2)
         */
        @Test
        public void test() {
        	BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
        	System.out.println(pre1.test("abc","abc"));
        
        	System.out.println("*******************");
        	
        	BiPredicate<String,String> pre2 = String :: equals;
        	System.out.println(pre2.test("abc","abc"));
        }
        
        
        /**
         * Function 中的 R apply(T t)
         * Employee 中的 String getName()
         */
        @Test
        public void test() {
        	Employee employee = new Employee(1001, "Jerry");
        
        	Function<Employee,String> func1 = e -> e.getName();
        	System.out.println(func1.apply(employee));
        
        	System.out.println("*******************");
        
        	Function<Employee,String> func2 = Employee::getName;
        	System.out.println(func2.apply(employee));
        }
        
  2. 构造器引用
    1. 格式:ClassName::new
    2. 与函数式接口相结合,自动与函数式接口中方法兼容
    3. 可以把构造器引用赋值给定义的方法
      • 函数式接口的抽象方法的形参列表和构造器的形参列表一致
      • 抽象方法的返回值类型即为构造器所属的类的类型
    4. 示例
      /**
       * Supplier 中的 T get()
       * Employee 的空参构造器:Employee()
       */
      @Test
      public void test(){
          Supplier<Employee> sup = new Supplier<Employee>() {
              @Override
              public Employee get() {
                  return new Employee();
              }
          };
          
          System.out.println("*******************");
      
          Supplier<Employee>  sup1 = () -> new Employee();
          System.out.println(sup1.get());
      
          System.out.println("*******************");
      
          Supplier<Employee>  sup2 = Employee :: new;
          System.out.println(sup2.get());
      }
      
      
      /**
       * Function 中的 R apply(T t)
       * Employee 的构造器:Employee(int id)
       */
      @Test
      public void test(){
          Function<Integer,Employee> func1 = id -> new Employee(id);
          Employee employee = func1.apply(1001);
          System.out.println(employee);
      
          System.out.println("*******************");
      
          Function<Integer,Employee> func2 = Employee :: new;
          Employee employee1 = func2.apply(1002);
          System.out.println(employee1);
      }
      
      
      /**
       * BiFunction 中的 R apply(T t,U u)
       * Employee 的构造器:Employee(int id,String name)
       */
      @Test
      public void test(){
           BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
           System.out.println(func1.apply(1001,"Tom"));
      
           System.out.println("*******************");
      
           BiFunction<Integer,String,Employee> func2 = Employee :: new;
           System.out.println(func2.apply(1002,"Tom"));
       }
      
  3. 数组引用
    1. 格式:type[ ] :: new
    2. 把数组看做是一个特殊的类,写法与构造器引用一致
    3. 示例
      /**
       * Function 中的 R apply(T t)
       */
      @Test
      public void test(){
          Function<Integer,String[]> func1 = length -> new String[length];
          String[] arr1 = func1.apply(5);
          System.out.println(Arrays.toString(arr1));
      
          System.out.println("*******************");
      
          Function<Integer,String[]> func2 = String[] :: new;
          String[] arr2 = func2.apply(10);
          System.out.println(Arrays.toString(arr2));
      }
      

5.强大的 Stream API

  1. Stream API 说明
    • Java8 中有两大最为重要的改变,第一个是 Lambda 表达式,另外一个则是 Stream API
    • 关系型数据库(Mysql、Oracle)可以在数据库层面处理数据,而非关系型数据库(MongDB、Redis)的数据就需要 Java 层面去处理
    • Stream 和 Collection 集合的区别
      • Stream 关注的是对数据的运算,与 CPU 打交道
      • Collection 关注的是数据的存储,与内存打交道
  2. 注意点
    • Stream 自己不会存储元素
    • Stream 不会改变源对象,相反,他们会返回一个持有结果的新 Stream
    • Stream 操作是延迟执行的,这意味着他们会等到需要结果的时候才执行
  3. Stream 执行流程
    • Stream 的实例化
      • 一个数据源(如:集合、数组),获取一个流
    • 中间操作
      • 一系列的中间操作,对数据源的数据进行处理(过滤、映射、…)
    • 终止操作
      • 一旦执行终止操作,就执行中间操作链,并产生结果,之后,不会再被使用
      • 图示
        在这里插入图片描述
  4. Stream 的实例化
    • 创建 Stream 方式一:通过集合
      /**
       * Java8 中的 Collection 接口被扩展,提供了两个获取流的方法
       * 		default Stream<E> stream() : 返回一个顺序流
       * 		default Stream<E> parallelStream() : 返回一个并行流
       */
      @Test
      public void test(){
      	List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
      
          Stream<Integer> stream = list.stream();
      
          Stream<Integer> parallelStream = list.parallelStream();
      }
      
    • 创建 Stream 方式二:通过数组
      /**
       * Java8 中的 Arrays 的静态方法 stream() 可以获取数组流
       * 		static <T> Stream<T> stream(T[] array): 返回一个流
       */
      @Test
      public void test(){
      	int[] arr = new int[]{1,2,3,4,5,6};
      
      	IntStream stream = Arrays.stream(arr);
       }
      
    • 创建 Stream 方式三:通过 Stream 的 of()
      /**
       * 可以调用 Stream 类静态方法 of(), 通过显示值创建一个流,它可以接收任意数量的参数
       * 		public static<T> Stream<T> of(T... values) : 返回一个流
       */
      @Test
      public void test(){
      	Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6,7);
      }
      
    • 创建 Stream 方式四:创建无限流
      /**
       * 可以使用静态方法 Stream.iterate() 和 Stream.generate(),创建无限流
       * 		迭代
       * 			public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) 
       * 		生成
       * 			public static<T> Stream<T> generate(Supplier<T> s) 
       */
      @Test
      public void test(){
          // 遍历前 10 个偶数
          Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);
      
          // 获取 10 个随机数
          Stream.generate(Math::random).limit(10).forEach(System.out::println);
      }
      
  5. Stream 的中间操作
    • 说明
      • 多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理,而在终止操作时一次性全部处理,称为“惰性求值”
    • 筛选与切片
      方法说明
      Stream<T> filter(Predicate<? super T> predicate)接收 Lambda , 从流中排除某些元素
      Stream<T> limit(long maxSize)截断流,使其元素不超过给定数量
      Stream<T> skip(long n)跳过元素,返回一个扔掉了前 n 个元素的流,若流中元素不足 n 个,则返回一个空流,与 limit(n) 互补
      Stream<T> distinct()筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
      @Test
      public void test(){
          // 获取一个包含多个 Employee 对象的集合,该方法为自定义
          List<Employee> list = EmployeeData.getEmployees();
      
          // filter(Predicate p)——接收 Lambda , 从流中排除某些元素
          // 查询员工表中薪资大于 7000 的员工信息
          list.stream().filter(e -> e.getSalary() >7000).forEach(System.out::println);
          
          // limit(n)——截断流,使其元素不超过给定数量。
          list.stream().limit(3).forEach(System.out::println);
      
          // skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流,与 limit(n) 互补
          list.stream().skip(3).forEach(System.out::println);
      
          // distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
          list.stream().distinct().forEach(System.out::println);
      }
      
    • 映射
      方法说明
      Stream<R> map(Function<? super T, ? extends R> mapper)接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
      Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
      DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
      IntStream mapToInt(ToIntFunction<? super T> mapper)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream
      LongStream mapToLong(ToLongFunction<? super T> mapper)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream
      @Test
      public void test(){
          // map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素
          List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
          list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);
      
          // 获取一个包含多个 Employee 对象的集合,该方法为自定义
          List<Employee> employees = EmployeeData.getEmployees();
      
          // 练习1:获取员工姓名长度大于 3 的员工的姓名
          employees.stream().map(Employee::getName).filter(name -> name.length() >3).forEach(System.out::println);
      
          // 练习2:对比 map 与 flatMap(Function f) 的区别,为此定义了 fromStringToStream 函数来使一维集合转为二维集合(StreamAPITest 为该类的名称)
          Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest::fromStringToStream);
          streamStream.forEach(s ->{
              s.forEach(System.out::println);
          });
          // flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
          Stream<Character> characterStream = list.stream().flatMap(StreamAPITest::fromStringToStream);
          characterStream.forEach(System.out::println);
      }
      
      // 将字符串中的多个字符构成的集合转换为对应的 Stream 的实例
      public static Stream<Character> fromStringToStream(String str){
          List<Character> list = new ArrayList<>();
          for(Character c : str.toCharArray()){
              list.add(c);
          }
         return list.stream();
      }
      
    • 排序
      方法说明
      Stream<T> sorted()产生一个新流,其中按自然顺序排序
      Stream<T> sorted(Comparator<? super T> comparator)产生一个新流,其中按比较器顺序排序
      @Test
      public void test(){
          // sorted()——自然排序
          List<Integer> list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7);
          list.stream().sorted().forEach(System.out::println);
      
          // 获取一个包含多个 Employee 对象的集合,该方法为自定义
          List<Employee> employees = EmployeeData.getEmployees();
          
          // sorted(Comparator com)——定制排序
          employees.stream().sorted( (e1,e2) -> {
             int ageValue = Integer.compare(e1.getAge(),e2.getAge());
             if(ageValue != 0){
                 return ageValue;
             }else{
                 return -Double.compare(e1.getSalary(),e2.getSalary());
             }
          }).forEach(System.out::println);
      }
      
  6. Stream 的终止操作
    • 说明
      • 终端操作会从流的流水线生成结果,其结果可以是任何不是流的值,例如:List、Integer,甚至是 void
      • 流进行了终止操作后,不能再次使用
    • 匹配与查找
      方法说明
      boolean allMatch(Predicate<? super T> predicate)检查是否匹配所有元素
      boolean anyMatch(Predicate<? super T> predicate)检查是否至少匹配一个元素
      boolean noneMatch(Predicate<? super T> predicate)检查是否没有匹配所有元素
      Optional<T> findFirst()返回第一个元素
      Optional<T> findAny()返回当前流中的任意元素
      long count()返回流中元素总数
      Optional<T> max(Comparator<? super T> comparator)返回流中最大值
      Optional<T> min(Comparator<? super T> comparator)返回流中最小值
      void forEach(Consumer<? super T> action)内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代,相反,Stream API 使用内部迭代——它帮你把迭代做了)
      @Test
      public void test(){
          // 获取一个包含多个 Employee 对象的集合,该方法为自定义
          List<Employee> list = EmployeeData.getEmployees();
      
          // allMatch(Predicate p)——检查是否匹配所有元素
          // 练习:是否所有的员工的年龄都大于 18
          boolean allMatch = list.stream().allMatch(e -> e.getAge() > 18);
          System.out.println(allMatch);
      
          // anyMatch(Predicate p)——检查是否至少匹配一个元素
          // 练习:是否存在员工的工资大于 10000
          boolean anyMatch = list.stream().anyMatch(e -> e.getSalary() > 10000);
          System.out.println(anyMatch);
      
          // noneMatch(Predicate p)——检查是否没有匹配的元素
          // 练习:是否存在员工姓“雷”
          boolean noneMatch = list.stream().noneMatch(e -> e.getName().startsWith("雷"));
          System.out.println(noneMatch);
      
          // findFirst——返回第一个元素
          Optional<Employee> findFirst = list.stream().findFirst();
          System.out.println(findFirst);
      
          // findAny——返回当前流中的任意元素
          Optional<Employee> findAny = list.parallelStream().findAny();
          System.out.println(findAny);
      
          // count——返回流中元素的总个数
          long count = list.stream().filter(e -> e.getSalary() > 5000).count();
          System.out.println(count);
          
          // max(Comparator c)——返回流中最大值
          // 练习:返回最高的工资
          Optional<Double> maxSalary = list.stream().map(e -> e.getSalary()).max(Double::compare);
          System.out.println(maxSalary);
          
          // min(Comparator c)——返回流中最小值
          // 练习:返回最低工资的员工
          Optional<Employee> minEmployee = list.stream().min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
          System.out.println(minEmployee);
          
          // forEach(Consumer c)——内部迭代
          list.stream().forEach(System.out::println);
      
          // 使用集合的遍历操作
          list.forEach(System.out::println);
      }
      
    • 归约(map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名)
      方法说明
      T reduce(T identity, BinaryOperator<T> accumulator)可以将流中元素反复结合起来,得到一个值,返回 T
      Optional<T> reduce(BinaryOperator<T> accumulator)可以将流中元素反复结合起来,得到一个值,返回 Optional<T>
      @Test
      public void test(){
          // reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值,返回 T
          // 练习1:计算 1-10 的自然数的和
          List<Integer> nums = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
          Integer sum = nums.stream().reduce(0, Integer::sum);
          System.out.println(sum);
      
          // 获取一个包含多个 Employee 对象的集合,该方法为自定义
          List<Employee> list = EmployeeData.getEmployees();
          
          // reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值,返回 Optional<T>
          // 练习2:计算公司所有员工工资的总和
          Stream<Double> salaryStream = list.stream().map(Employee::getSalary);
          // Optional<Double> sumMoney = salaryStream.reduce(Double::sum);
          Optional<Double> sumMoney = salaryStream.reduce((d1,d2) -> d1 + d2);
          System.out.println(sumMoney.get());
      }
      
    • 收集
      方法欧明
      <R, A> R collect(Collector<? super T, A, R> collector)将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法
      • Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)
      • Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例
      @Test
      public void test(){
          // 获取一个包含多个 Employee 对象的集合,该方法为自定义
          List<Employee> list = EmployeeData.getEmployees();
      
          // collect(Collector c)——将流转换为其他形式,接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法
          // 练习1:查找工资大于6000的员工,结果返回为一个List或Set
          List<Employee> employeeList = list.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());
          employeeList.forEach(System.out::println);
      
          Set<Employee> employeeSet = list.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet());
          employeeSet.forEach(System.out::println);
      }
      
  7. Collectors 类
    方法(均为静态方法)返回类型举例说明
    toListList<T>List<Employee> emps= list.stream().collect(Collectors.toList());把流中元素收集到 List
    toSetSet<T>Set<Employee> emps= list.stream().collect(Collectors.toSet());把流中元素收集到 Set
    toCollectionCollection<T>Collection<Employee> emps =list.stream().collect(Collectors.toCollection(ArrayList::new));把流中元素收集到创建的集合
    countingLonglong count = list.stream().collect(Collectors.counting());计算流中元素的个数
    summingIntIntegerint total=list.stream().collect(Collectors.summingInt(Employee::getSalary));对流中元素的整数属性求和
    averagingIntDoubledouble avg = list.stream().collect(Collectors.averagingInt(Employee::getSalary));计算流中元素 Integer 属性的平均值
    summarizingIntIntSummaryStatisticsint SummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));收集流中 Integer 属性的统计值,如:平均值
    joiningStringString str= list.stream().map(Employee::getName).collect(Collectors.joining());连接流中每个字符串
    maxByOptional<T>Optional<Emp> max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)));根据比较器选择最大值
    minByOptional<T>Optional<Emp> min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));根据比较器选择最小值
    reducing归约产生的类型int total=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));从一个作为累加器的初始值开始,利用 BinaryOperator 与流中元素逐个结合,从而归约成单个值
    collectingAndThen转换函数返回的类型int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));包裹另一个收集器,对其结果转换函数
    groupingByMap<K, List<T>>Map<Emp.Status, List<Emp>> map= list.stream().collect(Collectors.groupingBy(Employee::getStatus));
    partitioningByMap<Boolean, List<T>>Map<Boolean,List<Emp>> vd = list.stream().collect(Collectors.partitioningBy(Employee::getManage));根据 true 或 false 进行分区

6.Optional 类

  1. 作用:为了在程序中避免出现空指针异常而创建的
  2. Optional 类(java.util.Optional)是一个容器类,它可以保存类型T的值,代表这个值存在,或者仅仅保存 null ,表示这个值不存在,原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念,并且可以避免空指针异常
  3. 在 Javadoc 描述如下:这是一个可以为 null 的容器对象,如果值存在则 isPresent() 方法会返回 true ,调用 get() 方法会返回该对象
  4. Optional 类提供很多有用的方法,这样我们就不用显式进行空值检测
  5. 创建 Optional 类对象
    方法说明
    Optional.of(T t)创建一个 Optional 实例,t 必须非空
    Optional.ofNullable(T t)t 可以为 nul
    Optional.empty()创建一个空的 Optional 实例
  6. 判断 Optional 容器中是否包含对象
    方法说明
    boolean isPresent()判断是否包含对象
    void ifPresent(Consumer<? super T> consumer)如果有值,就执行 Consumer 接口的实现代码,并且该值会作为参数传给它
  7. 获取 Optional 容器的对象
    方法说明
    T get()如果调用对象包含值,返回该值,否则抛异常
    T orElse(T other) 如果单前的 Optional 内部封装的 t 是非空的,则返回内部的 t,如果内部的 t 是空的,则返回 orElse() 方法中的参数 t1
    T orElseGet(Supplier<? extends T> other)如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象
    T orElseThrow(Supplier<? extends X> exceptionSupplier)如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常
  8. 示例
    /**
     * Gil 对象是一个 JavaBean 对象,属性为 String name
     * Boy 对象是一个 JavaBean 对象,属性为 Girl girlFriend
     */
    // 使用 Optional 类的 getGirlName()
    public String getGirlName2(Boy boy){
         Optional<Boy> boyOptional = Optional.ofNullable(boy);
         
         // 此时的 boyNotNull 一定非空
         Boy boyNotNull = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));
    
         Girl girl = boyNotNull .getGirl();
         Optional<Girl> girlOptional = Optional.ofNullable(girl);
         
         // 此时的 girlNotNull 一定非空
         Girl girlNotNull = girlOptional.orElse(new Girl("古力娜扎"));
         return girlNotNull.getName();
     }
        
    @Test
    public void test() {
    	Boy boy = new Boy();
    	Optional<Girl> girl= Optional.ofNullable(boy.getGrilFriend());
    	// 如果 Girl 对象存在,就打印 Girl 对象的信息
    	girl.ifPresent(System.out::println);
    }
    
    @Test
    public void test() {
    	Boy boy = new Boy();
    	Optional<Girl> opt = Optional.ofNullable(boy.getGrilFriend());
    	// 如果有 Girl 对象就返回 Girl 对象,否则创建另外一个 Girl 对象
    	Girl girl = opt.orElse(new Girl("嫦娥"));
    	System.out.println("他的女朋友是:" + girl.getName());
    }
    
    
    /**
     * Employee 是一个 JavaBean 对象,其有两个属性
     * 		姓名:String name
     * 		工资:Double salary
     */
    @Test
    public void test(){
    	Optional<Employee> opt = Optional.of(new Employee("张三", 8888));
    	//判断opt中员工对象是否满足条件,如果满足就保留,否则返回空
    	Optional<Employee> emp = opt.filter(e -> e.getSalary()>10000);
    	System.out.println(emp);
    }
    
    @Test
    public void test(){
    	Optional<Employee> opt = Optional.of(new Employee("张三", 8888));
    	//如果 opt 中员工对象不为空,就涨薪10%
    	Optional<Employee> emp = opt.map(e ->{
    		e.setSalary(e.getSalary()%1.1);
    		return e;
    	});
    	System.out.println(emp);
    }
    
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以通过添加 ActionListener 来监听 JTable 上的按钮。首先,您需要创建一个 JButton,并将其添加到 JTable 中的每一行。然后,为每个按钮添加 ActionListener,并在监听器中处理按钮点击事件。 下面是一个简单的示例代码,演示如何在 JTable 上监听按钮点击事件: ```java import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; public class TableButtonListener { public static void main(String[] args) { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 创建表格模型 DefaultTableModel model = new DefaultTableModel(new Object[]{"Name", "Button"}, 0); // 向模型中添加数据 model.addRow(new Object[]{"John", new JButton("Button 1")}); model.addRow(new Object[]{"Jane", new JButton("Button 2")}); // 创建 JTable JTable table = new JTable(model); // 获取按钮列的单元格渲染器 TableColumn buttonColumn = table.getColumn("Button"); buttonColumn.setCellRenderer(new TableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { return (Component) value; } }); // 获取按钮列的单元格编辑器 buttonColumn.setCellEditor(new DefaultCellEditor(new JCheckBox()) { @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { return (Component) value; } }); // 添加按钮点击事件监听器 table.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { int row = table.rowAtPoint(e.getPoint()); int column = table.columnAtPoint(e.getPoint()); // 检查点击的是否是按钮列 if (column == table.getColumn("Button").getModelIndex()) { // 处理按钮点击事件 JButton button = (JButton) table.getValueAt(row, column); System.out.println("Button clicked: " + button.getText()); } } }); frame.add(new JScrollPane(table)); frame.pack(); frame.setVisible(true); } } ``` 在这个示例中,我们创建了一个包含两列的表格模型,其中一列是按钮。我们设置了单元格渲染器和编辑器,以便在表格中显示按钮。然后,我们通过添加鼠标点击事件监听器来处理按钮的点击事件。 当您点击按钮时,程序会打印出相应按钮的文本内容。您可以在监听器中根据需要执行其他操作。 希望这可以帮助到您!如果您有任何其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值