线程简介

  • 普通方法调用和多线程

    普通方法调用:主线程调用run()方法,主线程执行run()只有主线程一条执行路径

    多线程:主线程调用start()方法,子线程执行run();多条执行路径,主线程和子线程并行交替执行

  • 程序、进程Process线程Thread

    • 程序是指令和数据的有序集合,是一个静态的概念
  • 进程是程序的一次执行过程,是一个动态的概念。进程是资源分配的单位

    • 一个进程中可以包含若干个线程线程是CPU调度和执行的单位

      如视频中同时听声音,看图像,看弹幕。

    很多多线程是模拟出来的,真正的多线程是指有多个CPU,如服务器

  • 程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程gc线程

  • main()称之为主线程,为系统的入口,用于执行整个程序

  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的

  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制

  • 线程会带来额外的开销,如CPU调度时间并发控制开销

  • 每个线程在自己的工作内存交互内存控制不当会造成数据不一致

阅读全文 »

什么是异常

软件程序在运行过程中,可能会遇到各种异常问题Exception影响正常的程序执行流程。如:

  • 文件找不到
  • 网络连接失败
  • 非法参数

异常的简单分类

  • 检查性异常

    最具代表性的检查性异常是用户错误或问题引起的异常。这是程序员无法预见的,在编译时不能被简单地忽略。例如:要打开一个不存在的文件

  • 运行时异常

    运行时异常是可能被程序员避免的异常,可以在编译时被忽略

  • 错误Error

    错误不是异常,而是脱离程序员控制问题。错误在代码中通常被忽略。例如:栈溢出时,错误就发生了,这在编译时也检查不到。


异常体系结构

  • Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类
  • Java API中已经定义了许多异常类,这些异常分为两大类错误Error异常Exception

Error

  • Error类对象由JVM生成并抛出,大多数错误与代码编写者所执行的操作无关

  • Java虚拟机运行错误(Virtual MachineError)

    当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这样的情况发生时,JVM一般会选择线程终止

  • 还有的Error发生在虚拟机试图执行应用时

    • 类定义错误(NoClassDefFoundError)
    • 链接错误(LinkageError)

    这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。


Exception

  • 在Exception分支中有一个重要的子类RuntimeException

    • ArrayIndexOutOfBoundsException
    • NullPointerException
    • ArithmeticException
    • MissingResourceException
    • ClassNotFoundException

    这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。


异常处理机制

异常处理的五个关键字:

  1. try

  2. catch

  3. finally

  4. throw

    主动抛出异常,一般在方法中使用

  5. throws

    如果方法中出现处理不了的异常在方法上抛出异常

// IDEA中,Ctrl + ALt + T 可以对选中的代码迅速创建异常处理
try { // 监控区域
    new Test().test(1, 0);
} catch (ArithmeticException a) { // catch 想要捕获的异常类型
    
} catch (Exception e) { // 如果要捕捉多种异常,要按范围从小到大排序
    e.printStackTrace(); // 打印错误的栈信息
} catch (Throwable t) { 
    
} finally { // 可以不要
    // 里边的内容,无论是否捕获异常,都执行
}


/* 如果方法中出现处理不了的异常,在方法上抛出异常
	算数异常属于运行时异常,实际会自行抛出,不用特意写出throws,当前只是为了举例
*/
public void test (int a, int b) throws ArithmeticException {
	if (b == 0) {
        throw new ArithmeticException(); //主动抛出异常,一般在方法中使用
    }
    System.out.println(a/b);
}

自定义异常

  • 使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常。用户自定义异常类,只需继承Exception类即可

  • 自定义异常类的步骤

    1. 创建自定义异常类
    2. 在方法中通过throw关键字抛出异常对象
    3. 如果当前抛出异常的方法中处理异常,可以使用try-catch语句捕获并处理;否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作
    4. 在出现异常方法的调用者中捕获并处理异常
// 自定义的异常类
public class MyException extends Exception {

    private int detail;

    public MyException(int a) {
        this.detail = a;
    }

    @Override
    public String toString() { // 异常的打印信息
        return "MyException{" +
                "detail=" + detail +
                '}';
    }
}



public class Test {

    // 可能会存在异常的方法
    static void test(int a) throws MyException {

        System.out.println("传递的参数为:" + a);
        if (a > 10) {
            throw new MyException(a); // 抛出
        }

        System.out.println("OK");
    }

    public static void main(String[] args) {
        try {
            test(11);
        } catch (MyException e) {
            // 增加一些处理异常的代码
            System.out.println("MyException:" + e); // 会被调用toString方法
        }
    }
}

实际应用中的经验总结

  • 处理运行时异常时,采用逻辑去合理规避,同时辅助try-catch处理
  • 多重catch块后面,可以加一个catch(Exception e)处理可能被遗漏的异常
  • 对于不确定的代码,也可以加上try-catch,处理潜在的异常
  • 尽量去处理异常,切忌只是简单调用printStackTrace()打印输出,没有多少意义
  • 具体如何处理异常,要根据不同的业务需求和异常类型决定
  • 尽量添加finally语句释放占用的资源

什么是面向对象

  • 面向对象编程 (OOP, Object-Oriented Programming)
  • 面向对象的本质就是以类的方式组织代码,以对象的方式组织(封装)数据
  • 抽象
  • 三大特性
    • 封装
    • 继承
    • 多态

类与对象的关系

  • 类是一种抽象的数据类型,它是对某一类事物的整体描述/定义,但是并不能代表某一个具体的事物

  • 对象是抽象概念的具体实例

  • 认识论的角度是先有对象后有类。对象是具体的事物,类是对对象的抽象。

  • 代码运行角度是先有类后有对象。类是对象的模板。


创建与初始化对象

  • 使用new关键字创建对象

    创建对象除了会分配内存空间之外,还会给创建好的对象进行默认的初始化以及对类中构造器的调用


构造器详解

  • 类中的构造器也称为构造方法创建对象的时候必定会被调用

    • 使用new关键字,本质是在调用构造器

    • 一个类中即使什么内容都不写,它也会存在一个构造方法

  • 构造器的两个特点:

    1. 必须和类的名字相同
    2. 必须没有返回类型,也不能写void
  • 一旦定义了有参数的构造方法,无参构造方法必须显式定义才会有效


创建对象的内存分析

package com.hunter.oop;

public class Pet {
    public String name;
    public int age;

    public void shout() {
        System.out.println("叫了一声");
    }
}
package com.hunter.oop;

public class Application {
    public static void main(String[] args) {
        Pet dog = new Pet();
        dog.name = "旺财";
        dog.age = 3;
        dog.shout();

        System.out.println(dog.name);
        System.out.println(dog.age);

        Pet cat = new Pet();
    }
}

创建对象的内存示意图

Java内存分析


## 封装
  • 程序设计要追求高内聚、低耦合

    • 高内聚:尽可能类的每个方法只完成一件事
    • 低耦合:尽可能减少类的中的方法调用其他方法

    类的角度看:减少类内部对其他类的调用

    功能模块角度看:减少模块之间的交互复杂度

  • 封装

    封装就是对方法的实现细节进行隐藏。它是一种防止外界调用端去访问对象内部实现细节的手段。

  • 属性私有,提供公开接入的方法(getters/setters)

    代码更容易理解与维护,同时加强了代码的安全性。


继承

  • 继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。

  • 子类是父类的扩展(extends)

    • 子类继承了父类,就会拥有父类的全部方法和属性(private除外)
    • final修饰的类无法被继承
  • 继承是类和类之间的一种关系,此外还有依赖、组合、聚合等

  • Object类

    在Java中,所有的类都默认直接或间接继承Object类

  • Java类只有单继承,没有多继承

    一个子类只能有一个父类

super

  • super();

    调用父类的构造器,必须要在子类构造器的第一行

    如果父类定义了有参数的构造方法,且没有显式定义无参构造方法,那么子类就不能定义无参构造因此定义有参构造方法之前,最好先显式定义无参构造。

  • super只能出现在子类的方法或构造方法中

  • 不能同时用superthis调用构造方法

    因为两者都要求自己在第一行,会冲突。

super和this的比较

  • this表示本身调用者这个对象

  • super表示父类对象,只能在继承后使用

  • 构造方法

    this():本类的构造方法,可以简化代码的编写

    super():父类的构造方法


重写 Override

方法的重载

重写需要有继承关系,是子类对父类非静态的方法进行重写,与属性无关。

  • 方法名必须相同

  • 参数列表必须相同

  • 修饰符:范围可以扩大,但不能缩小

    public > protected > default > private

  • 抛出的异常:范围可以缩小,但不能扩大

    Exception > ClassNotFoundException

  • 静态方法和非静态方法区别很大

    • 静态方法:方法的调用只和左边定义的数据类型有关(不会被重写)

      public class A {
      	public static void test() {
              System.out.println("A.test()")
          }
      
      }
      
      
      public class B extends A {
      	public static void test() {
      		System.out.println("B.test()");
          }
      }
      
      
      public class Application {
          public static void main(String[] args) {
              
              B b = new B();
              b.test(); // 打印 B.test()
              
              A a = new B();
              a.test(); // 打印 A.test()
          }
          
      }
    • 非静态方法:重写

      public class A {
      	public void test() {
              System.out.println("A.test()")
          }
      
      }
      
      
      public class B extends A {
      	public void test() {
      		System.out.println("B.test()");
          }
      }
      
      
      public class Application {
          public static void main(String[] args) {
              
              B b = new B();
              b.test(); // 打印 B.test()
              
              A a = new B();
              a.test(); // 打印 B.test()
          }
          
      }
  • 为什么需要重写

    父类的功能子类不一定需要或不一定满足


多态

  • 多态就是同一方法可以根据发送对象的不同而采用多种不同的行为方式

    一个对象的实际类型是确定的,但指向的引用类型可以不确定

    // Student extends Person
    Student s1 = new Student(); 
    Person s2 = new Student(); // 父类可以指向子类,但不能调用子类独有的方法
    Object s3 = new Student(); 
    
    s2.run(); // 如果子类重写了父类的方法,会执行子类的方法
    ((Student) s2).eat(); // 如果是子类有的,而父类没有的方法,必须进行强制类型转换
  • 一个对象的实际类型是确定的,但可以指向对象的引用的类型有很多

  • 多态存在的条件

    1. 有继承关系
    2. 子类重写了父类方法
    3. 父类引用指向子类对象 Father f = new Son();
  • 多态是方法的多态,属性没有多态

    1. 静态方法(static)的调用只和左边定义的数据类型有关
    2. 常量(final)的调用也只和左边定义的数据类型有关
    3. private方法的调用也只和左边定义的数据类型有关

关键词 instanceof

关键词instanceof用于判断一个对象是什么类型

// Object > String
// Object > Person > Teacher
// Object > Person > Student
Object object = new Student();

System.out.println(object instanceof Student); // true
System.out.println(object instanceof Person); // true
System.out.println(object instanceof Object); // true
System.out.println(object instanceof Teacher); // false
System.out.println(object instanceof String); // false

Person person = new Student();
System.out.println(person instanceof Student); // true
System.out.println(person instanceof Person); // true
System.out.println(person instanceof Object); // true
System.out.println(person instanceof Teacher); // false
// System.out.println(person instanceof String); // 编译报错

System.out.println(X instanceof Y);语句能否编译通过要看X与Y之间是否有继承关系


引用类型的类型转换

Person obj = new Student();

((Student) obj).go() // 要使用【子类特有的方法】需要【强制类型转换】
  • 引用类型的类型转换可能会丢失自己本来的一些方法
    • 子类转换为父类是向上转型
    • 父类转换为子类为向下转型,属于强制转换
  • 引用类型的类型转换是为了方便方法的调用减少代码冗余

static关键字详解

代码块

publica class ClassName {
    {
        // 代码块(匿名代码块)
    }
    
    static {
        // 静态代码块
    }
    
}
  • 静态代码块

    加载类时就执行(只执行一次)

  • 匿名代码块

    • 创建对象自动创建,且在构造器之前创建
    • 运用场景:赋一些初始值

静态导入包

为了方便,想不加类名来使用工具类的静态方法,需要通过静态(static)导入包来实现:

import static java.lang.Math.random;

public class Application {
    public static void main(String[] args) {
        System.out.println(random());
    }
}

抽象类

  • abstract修饰符可以修饰方法,也可以修饰类。

    如果修饰方法,方法就是抽象方法;如果修饰类,类就是抽象类。

  • 抽象类中可以没有抽象方法,但有抽象方法的类一定要声明为抽象类

    抽象类中可以有普通的方法

  • 抽象类不能用new关键字来创建对象,它是用来让子类继承的

    抽象类不能被创建对象,但有构造器那么构造方法的作用是什么

    抽象类的构造方法可以用来初始化抽象类内部声明的通用变量,并被各种实现使用。

  • 抽象方法

    只有方法的声明,没有方法的实现,实现交由子类来实现。

  • 子类继承抽象类,就必须要实现抽象类没有实现的抽象方法否则该子类也要声明为抽象类


接口

  • 普通类:只有具体实现

  • 抽象类:具体实现和**规范(抽象方法)**都有

  • 接口:只有规范,自己无法写方法。使约束和实现分离面向接口编程

    声明接口的关键字是interface

  • 接口就是规范,定义的是一组规则,让不同的人实现。

  • OO(Object-Oriented)的精髓就是对对象的抽象最能体现这一点的就是接口

    设计模式所研究的,实际上就是如何合理地去抽象

  • 接口中定义属性和方法

    属性常量 public static final但一般不会在接口中定义属性方法抽象public abstract。因此接口中定义的属性和方法的修饰符可以省略

  •   public interface UserService {
          
          // 接口中的所有定义的方法其实都是抽象的 public abstract,因此修饰符可省略
          void run();
      
      }
      
    	
    - **类只能单继承,但接口可以多继承(可以实现多个接口)**
    
    	- 类通过关键字`implements`**实现接口**。**实现了接口的类,必须重写接口中的方法**。
    
    	- **实现接口的类的命名**通常以`Impl`结尾。
    
    	```java
    	
    	// 类可以通过implements实现多个接口
    	public class UserServiceImpl implements UserService, TimeService {
    	
    	    @Override
    	    public void add(String name) {
    	
    	    }
    	
    	    @Override
    	    public void timer() {
    	
    	    }
    	}
  • 接口中没有构造方法(抽象类有构造方法)


内部类

  • 内部类就是在一个类的内部再定义一个类

成员内部类

public class Outer {

    private int id = 10;
    public void out() {
        System.out.println("这是外部类的方法");
    }

    public class Inner {
        public void in() {
            System.out.println("这是内部类的方法");
        }

        // 获得外部类的私有属性
        public void getID() {
            System.out.println(id);
        }
    }
}


public class Application {
    public static void main(String[] args) {

        Outer outer = new Outer();
        
        //通过外部类来实例化内部类
        Outer.Inner inner = outer.new Inner();
        inner.in();
        inner.getID();

    }
}

静态内部类

public class Outer {

    private int id = 10;
    public void out() {
        System.out.println("这是外部类的方法");
    }

    public static class Inner {
        public void in() {
            System.out.println("这是内部类的方法");
        }

        // 无法获得外部类的私有属性,因为静态类会先加载,非静态属性只有创建对象时才会生成;改成静态属性后,可以访问
        /*
        public void getID() {
            System.out.println(id);
        }
        */
    }
}

局部内部类

局部内部类位于方法中

public class Outer {
	
	public void method() {
        // 局部内部类
		class Inner {
			public void in() {
			
			}
		}
	}

}

匿名内部类

没有类的名称,必须借助接口或者父类

public class Test {
    public static void main(String[] args) {
        //没有名字初始化类,不用将实例保存到变量中
        new Apple().eat();
    }
    
    // 变成了实现了接口的类(匿名内部类)
    UserService userService = new UserService() {
        @Override
        public void hello() {

        }
    }
}

class Apple {
    public void eat() {
        System.out.println("1");
    }
}

interface UserService {
    void hello();
}

  • 一个Java类中可以有多个class类,但只能有一个public class

    public class Outer {
        
    }
    
    
    // 可以作为测试类,对public class类进行测试
    class A {
        public static void main(String[] args) {
            
        }
    }

常用API

Object 类

概述

java.lang.Object类是Java语言中所有类的父类,其中描述的所有方法,子类都可以使用。如果一个类没有特别指定父类,则默认继承自Object类


toString 方法

直接打印对象的变量名,其实就是调用toString方法(对象在堆内存中的地址值)。但是Object默认的toString方法获取对象的地址值其实没有多少意义和可读性。

自定义的类,一般会重写toString方法,获取对象的属性


equals 方法

默认地址比较

Object默认的equals方法为:使用==运算符比较对象地址只要不是同一个对象,结果必然为false

对象内容比较

如果希望进行对象的内容比较,则可以重写equals方法。equals方法隐含着一个多态(传入的是Object类的对象)

public boolean equals(Object obj) {...}
  • 可选:
    • 添加一个判断,传递的参数obj如果是this本身,直接返回true,提高程序效率
    • 添加一个判断,传递的参数obj如果是null,直接返回false,提高程序效率
  • 添加一个判断,防止类型转换异常ClassCastException
  • 使用强制类型转换,把obj类转换成需要的类型
  • 再比较两个对象的属性
//idea自动生成的equals方法的重写
@override
public boolean equals(Object o) {
    if (this == o) return true; // this本身
    if (o == null || getClass() != o.getClass()) // o是null值 或 不能进行类型转换(反射技术,等效于 (o instanceof 当前对象的类型) )
        return false;
    当前类 x = (当前类) o; // 强制类型转换
    return Objects.equals(this.strAttribute, o.strAttribute) && this.intAttribute == o.intAttribute; // strAttribute.equals(o.strAttribute) 是Object的equals方法,容易抛出空指针异常;因此采用Objects类的equals方法
}

clone 方法

clone方法可以实现对象的复制clone方法要求子类实现java.lang.Clonable接口,从而在子类中实现对象的复制。

浅克隆:对象在复制时仅复制基本类型的属性值到新对象中引用的变量不会被复制

深度克隆不仅复制基本类型的属性值到新对象中引用的变量本身也会被复制

实现深度克隆

要实现深度克隆,需要重写clone方法


Objects 类

JDK 1.7添加了一个java.util.Objects工具类,它提供了一些方法来操作对象,这些方法是**null-save(空指针安全)**的,用于计算对象的hashcode、返回对象的字符串表示形式、比较两个对象

在比较两个对象的时候,Object的equals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.euqals(b));
}

日期时间类

Date 类

java.util.Date类表示特定的瞬间,精确到毫秒

  • 构造方法:
    • public Date()
    • public Date(long date):分配Date对象并初始化此对象,以表示自从标准基准时间(1970年1月1日00:00:00 GMT)以来的指定毫秒数

简单来说,使用无参构造,可以自动设置当前系统时间的毫秒时刻,指定long类型可以自定义毫秒时刻;输出Date类时,会自动转换为日期格式

  • 成员方法:

    long getTime():把当对象表示的时间转换为毫秒值


DateFormat类

java.text.DateFormat是日期/时间格式化子类的抽象类,可以通过这个类完成日期和文本之间的转换,即在Date对象和String对象之间来回转换。

  • 格式化

    按照指定的格式,从Date对象转换为String对象;String format(Date date)

  • 解析

    按照指定的格式,从String对象转换为Date对象;Date parse(String source)


构造方法

由于DateFormat为抽象类,因此需要常用的子类java.text.SimpleDateFormat。这个类需要指定格式化或解析的格式

构造方法为:public SimpleDateFormat(String pattern)

参数pattern是一个字符串,代表日期时间的自定义格式,常用的格式规则如下:

标识字母(区分大小写 含义
y
M
d
H
m
s

格式中的字母不能更改,但连接的符号可以改变

// 1. 创建SimpleDateFormat对象,构造方法中传递指定的格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
//SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
  • 格式化

    调用SimpleDateFormat对象中的format方法,按照构造方法中指定的模式,把Date日期格式化为符合模式的字符串

    Date date = new Date();
    String d = sdf.format(date);
    System.out.println(d);
  • 解析

    调用SimpleDateFormat对象中的parse方法,把符合构造方法中的格式的字符串解析为Date对象。

    ​ 注意:parse方法声明了一个ParseExceptoion,如果字符串和构造方法的格式不一样,就会抛出异常。

    Date date = sdf.parse("2021/07/31 22:26:33");
    System.out.println(date);

Calendar 类

java.util.Calendar在Date类之后出现,是一个抽象类,替换掉了很多Date的方法。该类将所有可能用到的时间信息封装为静态成员变量,方便获取

字段值 含义
YEAR
MONTH 月(0-11)
WEEK_OF_YEAR 一年中的第几周
WEEK_OF_MONTH 一个月中的第几周
DATE
DAY_OF_MONTH
DAY_OF_WEEK 一周中的第几天
DAY_OF_YEAR 一年中的第几天
HOUR
MINUTE
SECOND
MILLISECOND 毫秒

获取方法

Calendar类无法直接创建对象使用,它有一个静态方法getInstance(),该方法使用默认时区和语言环境返回Calendar类的子类对象


常用方法
  • public int get(int field):返回给定日历字段的值(参数使用Calendar类的静态成员变量
  • public void set(int field, int value):将给定的日历字段设置为给定值
  • public void set(int year, int month, int day):同时设置年月日
  • public abstract void add(int field, int amount):根据日历的规则,为给定的日历字段添加或减去指定的时间量
  • public Date getTime():返回一个表示此Calendar时间值(从历元到现在的毫秒偏移量)的Date对象
Calendar c = new Calendar.getInstance();
int year = c.get(Calender.YEAR);
System.out.println(year);

Math 类


BigInteger 类


BigDecimal 类


System 类

java.lang.system类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作,在System类的API文档中,常用的方法有:

  • public static long currentTimeMills():返回以毫秒为单位的当前时间(和DATE类能达到相同的效果),可以用来测试程序的效率
  • public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length):将数组中指定的数据拷贝到另一个数组中
    • src 源数组
    • srcPos 起始位置
    • dest 目标数组
    • destPos 目标数组中的起始位置
    • length 要复制的数组长度

currentTimeMills 方法

long s = System.currentTimeMills();
for (int i= 0; i < 10000; i++) {
    System.out.println(i);
}
long e = System.currentTimeMills();
System.out.println("程序共耗时:" + (e - s) + "毫秒");

arraycopy 方法

int[] src = {1, 2, 3, 4, 5};
int[] dest = {6, 7, 8, 9, 10};
System.arraycopy(src, 0, dest, 0, 3);
System.out.println(Arrays.toString(dest));

StringBuilder 类

又称为字符串缓冲区,可以提高字符串的操作效率(看作一个长度可变的字符串)。底层是一个初始长度为16的数组,但是没有被final修饰,因此长度可变。


构造方法

  • public StringBuilder():构造一个空的StringBuilder容器
  • public StringBuilder(String str):构造一个StringBuilder容器,并将字符串添加进去

常用方法

  • public StringBuilder append(...):添加任意类型数据,会将其内容转换为字符串形式,并返回当前对象自身,因此无需接收返回值

  • public String toString():将当前StringBuilder对象转换为String对象

数组的定义

  • 数组是相同数据类型的有序集合

特点:

  • 长度确定

    数组一旦被创建,它的大小就是不可改变的

  • 数组中的元素可以是基本类型,也可以是引用类型

  • 数组变量属于引用类型的数据

    数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此无论数组中的元素的数据类型,数组对象本身就在堆中


声明创建数组

  • 首先必须声明数组变量,才能在程序中使用数组

    dataType[] arrayRefVar; // 首选的方法
    或
    dataType arrayRefVar[]; // 效果相同,但不是首选方法(C/C++风格)
  • Java语言使用new操作符来创建数组

    dataType[] arrayRefVar = new dataType[arraySize];
  • 获取数组长度

    arrays.length

Java内存分析

  • 存放基本类型变量(包含这个基本类型的具体数值)

  • 引用类型的变量(存放这个引用在堆里面的具体地址)

    对象是通过引用来操作的


  • 存放new对象数组,不会存放别的对象引用
  • 可以被所有的线程共享

方法区

  • 包含了所有的class和static变量

数组的三种初始化

静态初始化

int[] a = {1, 2, 3};
Man[] men = {new Man(1, 1), new Man(2, 2)};

动态初始化

int[] a = new int[2];
a[0] = 1;
a[1] = 2;  

数组的默认初始化

数组是引用类型,它的元素相当于类的实例变量。因此数组一经分配空间,其中的每个元素也被按照实例变量的方式被隐式初始化


对数组进行反转

public static int[] reverse(int[] arrays) {
    int[] result = new int[arrays.length];
    
    for(int i = 0, j = result.length-1; i < arrays.length; i++, j--) {
        result[j] = arrays[i];
    }
    
    return result;
}

多维数组

  • 多维数组可以看成是数组的数组

    比如二维数组就是一个特殊的一维数组,其中每一个元素都是一个一维数组

    // 一个两行五列的数组
    int a[][] = new int[2][5];

Arrays类

  • 数组的工具类java.util.Arrays

  • 由于数组对象本身并没有什么方法可供调用,但API中提供了一个工具类Arrays,从而可以对数据对象进行一些基本操作

  • 查看JDK帮助文档

  • Arrays类中的方法都是static修饰的静态方法,在使用的时候可以直接使用类名进行调用,而不用使用对象来调用(使用对象调用也能实现,但没必要)

  • 常用功能(详细用法可见帮助文档)

    • 给数组赋值:fill
    • 对数组排序:sort升序
    • 比较数组:equals,比较数组中元素值是否相等
    • 查找数组元素:binarySearch方法能对排序好的数组进行二分查找法操作
    • 将数组转换为List类型:asList

冒泡排序

  1. 比较数组中相邻的元素,如果第一个数 > 第二个数,就交换位置;
  2. 每一次比较,都会产生出一个最大的数字如果第一个数 < 第二个数交换位置,则会产生一个最小的数字);
  3. 下一轮可以少一次排序;
  4. 依次循环,直到结束。
public static void sort(int[] arrays) {
    int tmp = 0;
    
    // 外层循环,判断我们要走多少次
    for (int i = 0; i < arrays.length - 1; i++) {
        boolean flag = false; // 通过flag标识减少数组有序后的无意义的比较
        
        // 内层循环,比较判断两个数
        for (int j = 0; j < arrays.length - 1 - i; j++) {
            if (arrays[j+1] > arrays[j]) {
                tmp = arrays[j];
                arrays[j] = arrays[j+1];
                arrays[j+1] = tmp;
                flag = true; // 有调整顺序
            }
        }
        
        if (flag == false) { // 一次循环中没有调整顺序,说明数组实际已经有序,不必再循环
            break;
        }
    }
    
    return arrays;
}

稀疏数组

  • 当一个数组中大部分元素为0,或者为同一值的数时,可以使用稀疏数组来保存。

  • 处理方式

    • 记录数组一共有几行几列,和大部分元素值不同的个数
    • 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
    //将普通数组转换为稀疏数组
    
    // 获取有效值的个数
    int cnt = 0;
    for (int i = 0; i < 11; i++) {
        for (int j = 0; j < 11; j++) {
            if (arr[i][j] != 0) {
                cnt++;
            }
    	}
    }
    
    // 创建一个描述稀疏数组的数组
    int[][] arr2 = new int[cnt+1][3];
    
    arr2[0][0] = 11; //行数
    arr2[0][1] = 11; //列数
    arr2[0][2] = cnt; // 和大部分元素值不同的个数
    
    //遍历二维数组,将非零的值存放在稀疏数组中
    int row = 0;
    for (int i = 0; i < arr.length; i++) {
        for (int j = 0; j < arr[i].length; j++) {
            if (arr[i][j] != 0) {
                row++;
                arr2[row][0] = i;
                arr2[row][1] = j;
                arr2[row][2] = arr[i][j];
            }
        }
    }
    
    
    // 读取稀疏数组
    int[][] arr3 = new int[arr2[0][0]][arr2[0][1]];
    
    Array.fill(arr3, 0); //填充洗漱数组中大部分相同的元素
    
    // 将少部分不同的元素填充进去
    for (int i = 1; i < arr2.length; i++) {
        arr3[arr2[i][0]][arr2[i][1]] = arr2[i][2];
    }

方法的定义

  • Java的方法类似于其它语言的函数,是一段用来完成特定功能代码片段

  • 方法包含一个方法头和一个方法体

  •   修饰符 返回值类型 方法名(参数类型 参数名) {
          方法体
              
          return 返回值;
      }
      
    
    - 方法**可以不包含任何参数**
    
    ---
    
    ## 值传递和引用传递
    
    ### 值传递 pass by value
    
    在调用函数时,**将实际参数复制一份**传递到函数中,这样在行数中对参数进行修改,就**不会影响到原来的实际参数**。
    
    ### 引用传递 pass by reference
    
    在调用函数时,**将实际参数的地址**直接传递到函数中,这样在函数中对参数进行的修改就**会影响到实际参数**。
    
    ---
    
    ### Java是值传递
    
    - Java中,调用函数时,如果参数是**对象**,**实际参数**其实就是**指向对象的地址**。
    
    	如果传递的是地址,就**看这个地址的变化**,而**不是看地址指向的对象的变化**。
    
    - 值传递和引用传递的**区别并不是传递的内容**,而是**实际参数是否被复制**并传递到函数。
    
    ---
    
    ## 方法的重载
    
    - 重载就是在一个类中,有**相同的函数名称**,但**参数不同**的函数。
    
    - 方法的重载规则
    
    	- **方法名称**必须**相同**
    	- **参数列表**必须**不同**(**个数**不同/**类型**不同/参数**排列顺序**不同等)
    	- 方法的**返回类型不限制**(可以相同也可以不同)
    	- 仅仅返回类型不同不足以成为方法的重载
    
    - 实现理论
    
    	方法名称相同时,编译器会根据调用方法的参数个数、类型等去逐个匹配,以选择对应的方法,如果匹配失败,则编译器报错。
    
    ---
    
    ## 可变参数
    
       - JDK 1.5开始,Java支持传递**同类型的可变参数**给一个方法
    
       - 在方法声明中,在指定参数类型后加一个省略号`...`
    
       - **一个方法中只能指定一个可变参数**,它**必须是方法的最后一个参数**
    
       - ```java
    	public class Demo {
    		public static void main(String[] args) {
    	        printMax(34, 4, 6, 78, 232.4);
    	        printMax(new double[]{1, 2, 3})
    	    }
    	    
    	    public static void printMax(double... numbers) { // 可变参数
    	        if (numbers.length == 0) {
    	            System.out.println("No argument passed");
    	            return;
    	        }
    	
    	        double result = numbers[0];
    	
    	        // 获取最大值
    	        for (int i = 0; i < numbers.length; i++) {
    	            if (numbers[i] > result) {
    	                result = numbers[i];
    	            }
    	            System.out.println("The max value is " + result);
    	        }
    	    }
    	}

用户交互Scanner

java.util.ScannerJava5的特性。可以通过Scanner类获取用户的输入

  • 通过Scanner类的next()nextLine()方法获取输入的字符串,在读取前我们一般需要使用hasNext()hasNextLine()判断是否还有输入的数据

    • next()

      • 一定要读取到有效字符后才会结束输入;
      • 对输入有效字符前遇到的空白next()会将其自动去掉
      • 只有输入有效字符后才将其后面输入的空白作为分隔符或结束符
      • ==next()不能得到带有空格的字符串==
    • nextLine()

      • Enter为结束符
      • 可以获得空白
Scanner s = new Scanner(System.in);
// 判断用户有没有输入字符串
if (scanner.hasNext()) {
    String str = scanner.next();
    System.out.println("输出的内容为:" + str);
}
// 凡是属于I/O流的类,如果不关闭会一直占用资源。用完就关掉。
scanner.close();
  • 凡是属于I/O流的类,如果不关闭会一直占用资源。

    用完就关掉scanner.close()


Switch选择结构

  • 除了if else语句外,多选择结构还可以通过switch case语句实现
  • switch case语句判断一个变量一系列值中的某一个值是否相等,每个值称为一个分支
  • switch 语句中的变量类型可以是
    • byte、short、int、char
    • JDK 7开始,switch支持String类型
    • case标签必须是字符串常量字面量
switch (expression) {
    case value:
        // 语句
        break; // 可选,但最好写上
    case value:
        // 语句
        break; // 可选,但最好写上
    // 可以有任意数量的case语句
    default: // 可选,放最后
        // 语句 
}
  • ==case穿透==

    如果分支中没有break,当变量成功匹配上一个分支后,之后的分支会顺序执行,直到语句结束或遇到break。

    因此,规范写法:每个分支必须要有break

  • default

    变量值没能与其他分支匹配时,会执行default分支

    因此理论上应该放在最后


循环

while循环


do while循环

do {
	// 代码语句
} while (布尔表达时);
  • while 和 do while循环的区别

    while先判断后执行,do while 先执行后判断

  • 记住while之后要加分号;,当然,好的IDE都会相关有纠错提示


for循环


  • for循环使一些循环结构变得更加简单;

  • for循环语句是支持迭代的一种通用结构,是最有效、最灵活的循环结构;

  •   for (初始化; 布尔表达式; 迭代) {
          // 代码语句
      }
      
    
    ---
    
    ## 增强for循环 foreach
    
    - **Java5** 引入了主要用于**数组或集合**的增强型for循环;
    
    - ```java
    	for (声明语句 : 表达式) {
    	    // 代码语句
    	};
  • 声明语句

    • 声明新的局部变量,该变量的类型必须和数组元素的类型匹配
    • 作用域限定在循环语句块,其值与此时数组元素的值相等;
  • 表达式

    表达式时要访问的数组名,或者是返回值为数组的方法;

int[] numbers = {10, 20, 30, 40};

for (int x : numbers) {
    System.out.println(x);
}

break、continue、goto

break

break用于强行退出循环,不执行剩余语句(break语句也在switch语句中使用)

continue

continue语句用在循环语句体中,用于终止某次循环过程。即跳过循环体中尚未执行的语句,直接进行下一次是否执行循环的判定

goto

goto关键字很早就在程序设计语言中出现。尽管goto仍然是Java的一个保留字,但并未得到正式使用;Java没有goto

但在break和continue身上,我们能看出一些goto的影子——带标签的break和continue。

  • 标签是指后面跟一个冒号的标识符,如label:

  • 对Java来说,唯一用到标签的地方是在循环语句之前

    设置标签的唯一理由:我们希望在其中嵌套另一个循环,由于break和continue关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方

outer: for (int i = 0; i< 9; i++) {
    forint j = 2; j < i / 2; j++) {
        if (i % j == 0) {
            continue outer;
        }
    }
    System.out.print(i+" ");
}

日常使用

  1. 快速输入public static void main(String[] args){}

    psvm + tab即可

  2. 快速输入System.out.println();

  • sout + tab即可
  • 可以先输入任何想要输出的变量,再.sout
  1. 创建对象或使用一个能返回对象的方法时,直接写new ClassName()/ClassName.methodName(parameters),再Alt + Enter,即可自动产生对象名。

  2. 谷歌IDEA优化,进行相关操作

  3. 运行Java文件

  • 上次运行过的文件:Shift + F10
  • 当前文件:Ctrl + Shift + F10
  1. 反编译

    1. 工具栏 - Project Structure… - Project - Project compiler output,找到编译成字节码的class文件输出地址,在文件资源管理器中打开该地址;
    2. IDEA中,在中意的Package右键 - Show in Explore
    3. 在文件资源管理器中,复制class文件到java文件所在的目录,之后再在IDEA中打开,即可查看反编译后的内容
  2. 编写构造器

    Alt + insert –> Constructor

  3. 编写Getter和Setter方法

    Alt + insert –> Getter and Setter

  4. 方法的重载

    Alt + insert –> Override Methods…

    // 创建重载方法后,IDEA 会自动写一个注解(有功能的注释)
    @Override
    public void overrideMethod() {
        // 方法体
    }
  5. 包裹选中的代码

    Ctrl + Alt + T

  6. 自动清除无效import

    Settings -> Editor -> General -> Auto Import -> Java

    勾选Optimize imports on the fly

    image-20220818002554092

  7. 在底部的Git标签页中,添加Local Changes标签页

    效果:

    image-20221214231836532

    操作:File - Settings - Version Control - Commit - 取消勾选Use non-modal commit interfce

    image-20221214231903047
  8. 去掉import自动合并

IDEA java文件import去掉自动合并

  1. idea+tomcat实现热部署-修改java代码及时生效

  2. Endpoints:查看web应用程序的请求映射

    view - Tool Windows - Endpoints

    image-20240729143156072 image-20240729143247150

日志

设置快捷生成日志的提示:

  1. File - Settings - Editor - Live Templates - 加号 - Live Template

image-20230917005706530

  1. Abbreviation填写期望的快捷缩写,Template text填写期望的填充效果

    private static final Logger LOGGER = LoggerFactory.getLogger($CLASS_NAME$.class);

  2. Edit Variables… - Expression选择className()

  3. 左下角Define,设置生效的场景:java: declaration

    image-20230917012446212

文件和代码模板

创建类时自动添加注释

  • File - Settings - Editor - File and Code Tmplates
  • Scheme: Default
  • Includes - File Header
image-20231009100131089

Debug

断点

IDEA中有4种基础断点:

  1. 行断点
  2. 方法断点
  3. 属性断点
  4. 异常断点

以此4种断点为基础,衍生出了4种断点:

  1. **条件断点。**右键断点,添加Condition。只有在满足条件时,断点才会阻断程序运行。
image-20240717110843181
  1. 源断点。可以在不阻断代码的情况下,在控制台上输出某些信息。

Shift+鼠标左键 添加断点,勾选"Breakpoint hit" messageEvaluate and log

  • "Breakpoint hit" message:在调试控制台中输出断点被触发的消息。默认消息为 “Breakpoint hit at <文件名>:<行号>”。
  • Evaluate and log:执行输入框中输入的表达式,并将结果输出到控制台。

image-20240717114900903

  1. 多线程断点。模拟线程的轮询执行。

右键断点,Suspend选择Thread。实际debug过程中,可以在Debugger的标签页看到不同的线程,通过切换线程进行Debug,就能模拟线程的轮询执行。

image-20240717142624986
  1. **Stream断点。**在Stream的代码行加上断点,就可以在运行到这行代码时,点击Trace Current Stream Chain,看到数据的流转情况。

image-20240717142837288


进入断点之后的调试技巧

  1. Step Over:一行行往下执行,不会进入方法内部
Step Over
  1. Step Into:进入方法内部。但是一些外部Jar包的方法会无法进入。如果想要进入外部Jar包方法,可以使用Force Step Into
Step Into Force Step Into
  1. Step Out:跳转到调用方法处。

  2. Run to Cursor:跳转到鼠标光标所在位置。

image-20240717144632995

回退重新执行方法 Reset Frame

在调用栈中,点击希望回退的位置对应方法左侧的回退图标Reset Frame,就能实现回退。

image-20240717144332410


跳转至下一个断点 Resume Program

如果没有后续断点,就会将程序执行至结束。

image-20240717144557317

观察指定变量的变化情况 Watches

Debug过程中,所有的变量都会被展示在Debugger栏中,如果只想要观察某几个特定的变量,不便于观察

勾选Debug标签页右侧Layout Settings下的Separate Watches选项。再添加一个想要监听的变量或表达式,就能只关注自己关心的变量信息。

image-20240717145055989

手动修改变量的值

通过Evaluate Expression...就可以实现手动修改变量值,以快速进入某个代码逻辑中

image-20240717145755575

手动抛出异常,测试异常处理的代码逻辑

选择栈中方法后,右键选择Throw Exception

image-20240717150036571

例如构造一个空指针异常:

image-20240717150152071

监控断点的开销

有时候打了断点会让项目执行很慢,尤其是一些复杂的条件断点。勾选Overhead,就能看到断点的执行次数和执行耗时。

如果发现断点耗时,可以去掉,节约资源。

image-20240717150846260

添加JVM启动参数

Edit Configurations... Modify options Add VM options

添加JVM启动参数

  • -Xms:堆内存最小值,例如-Xms1m

  • -Xmx:堆内存最大值,例如-Xmx1m

  • -Xss:指定线程栈的大小

    通常不需要修改值,如果遇到栈溢出报错,可以适当增大;如果需要创建大量线程,可以适当减小。

  • -XX:+HeapDumpOnOutOfMemoryError:发生OOM后,生成相应的dump文件

  • -XX:HeapDumpPath=[指定的存放路径]:dump文件存放路径

  • -Xlog:gc*:查看GC日志

  • -XX:MaxTenuringThreshold:设置对象从Survivor区晋升到老年代的年龄阈值

  • -Dserver.port=[目标端口号]:指定运行的端口


Spring Boot多实例运行

参考:IntelliJIDEA中实现Spring Boot多实例运行:修改配置与批量启动详解-CSDN博客

允许多实例

右上角工具栏 - 选择Edit Configurations...

image-20250325212945477

选择目标Spring Boot项目 - Modify options - Allow multiple instances(好像也不用勾选?)

image-20250325212914570


指定端口

参照添加JVM启动参数章节,添加-Dserver.port=[目标端口号],保存。


批量操作

Copy Configuration…,指定不同的端口。点击IDEA左下角的Services,如果没有显示Spring Boot项目的启动类,选择Run Configuration... - Spring Boot,就能够显示。

image-20250325221330811 image-20250325221407334

可以进行批量操作,启动后会显示服务对应的端口:

image-20250325221639059

方式二?直接Copy Configuration

image-20250423171706118

绪论

机器学习研究的主要内容,是关于在计算机上从数据中产生模型(model)的算法,即学习算法(learning algorithm)

《机器学习》一书中,模型泛指从数据中学得的结果,有时将模型称为学习器learner,可以看作学习算法在给定数据和参数空间上的实例化。有些文献,模型全局性结果,如一棵决策树模式局部性结果,如一条规则


基本术语

  • 数据集 data set:记录的集合

    • 示例/样本 instance/sample:关于一个事件或对象的描述。
  • 属性/特征 attribute/feature:反映事件或对象在某方面的表现或性质的事项。

  • 样本空间 sample space属性张成的空间。

    e.g. 把色泽、根蒂、敲声作为三个坐标轴,则张成一个用于描述西瓜的三维空间,每个西瓜都可以找到自己的坐标位置。

  • 一个样本也可以称为样本空间中的一个特征向量 feature vector

$D={x_1,x_2,…,x_m}$表示包含$m$个示例的数据集,每个示例包含$d$个属性描述。每个示例$x_i=(x_{i1};x_{i2};…;x_{id})$是d维样本空间中的一个向量。

  • 学习/训练 learning/training:从数据中学得模型的过程,这个过程通过执行某个学习算法来完成,学习过程就是为了找出或逼近真相

    • 训练数据 training data:训练过程中使用的数据

    • 训练样本 training sample

    • 训练集 training set:训练样本组成的集合

    • 标记 label:训练样本的结果信息

      拥有了标记label 的示例sample,称为样例example,一般用$(x_u,y_i)$表示第i个样例,$y_i$是示例$x_i$的标记。

    • $Y$是所有标记label的集合,称为标记空间label space/输出空间

  • 假设 hypothesis

    学得的模型对应了关于数据的某种潜在的规律

  • 真实 ground-truth客观规律自身

  • 预测模型 prediction model

  • 分类 classification

    预测离散值的学习任务。例如好瓜、坏瓜。

    • binary classification 二分类
      • positive class 正类
      • negative class 反类
    • multi-class classification 多分类

    一般地,预测prediction任务希望通过对训练集training set进行学习建立一个从输入空间X到输出空间Y的映射

  • 回归 regression

    预测连续值的学习任务。例如西瓜成熟度0.95、0.37。

  • 测试 testing

    testing sample 被预测的样本

  • 聚类 clustering

    将训练集中的西瓜分成若干组,每组称为一个簇cluster

    这些自动形成的簇可能对应一些潜在的概念划分,例如“浅色瓜”、“深色瓜”。

  • 监督学习 supervised learning

    训练数据拥有标记信息

    • 分类、回归
  • 无监督学习 unsupervised learning

    训练数据没有标记信息

    • 聚类
  • 泛化 generalization

    学得模型适用于新样本的能力,称为泛化能力。

通常假设样本空间中全体样本服从一个未知分布(distribution)D,我们获得的每个样本都是独立地从这个分布上采样获得的,即**“独立同分布”(independent and identically distributed)**


假设空间

  • 归纳 induction

    从特殊到一般泛化(generalization)过程,即从具体的事实归结出一般性规律

    从样例中学习显然是一个归纳的过程,因此亦称归纳学习(induction learning)

  • 演绎 deduction

    从一般到特殊特化(specialization)过程,即从基础原理推演出具体状况

可以把学习过程看作一个在所有假设(hypothesis)组成的空间中进行搜索的过程,搜索目标是找到与训练集**匹配(fit)**的假设。假设的表示一旦确定,假设空间及其规模大小就确定了。

现实问题中我们常面临很大的假设空间,但学习的过程是基于有限样本训练集进行的,因此可能有多个假设与训练集一致与训练集一致的假设集合,称之为版本空间(version space)


归纳偏好

对于一个具体的学习算法而言,它必须要产生一个模型。学习算法本身的偏好会起到关键作用。

机器学习算法在学习过程中对某类假设的偏好,称为归纳偏好(inductive bias)

是否存在一般性的原则来引导算法确立“正确”的偏好?

**“奥卡姆剃刀”(Occam’s razor)**原则

奥卡姆剃刀原则是一种常用的、自然科学研究中最基本的原则:若有多个假设与观察一致,则选最简单的那个

然而,奥卡姆剃刀并非唯一可行的原则,且其本身存在不同的诠释。例如:

  1. 好瓜↔(色泽 = *) ^ (根蒂 = 蜷缩) ^ (敲声 = 浊响)
  2. 好瓜↔(色泽 = *) ^ (根蒂 = 蜷缩) ^ (敲声 = *)

这两个假设何者更简单的问题并不简单,需要借助其他机制才能解决。


数据挖掘与机器学习的联系

数据库领域的研究为数据挖掘提供数据管理技术

机器学习和统计学的研究为数据挖掘提供数据分析技术;(统计学主要通过机器学习对数据挖掘发挥影响)


模型评估与选择

经验误差与过拟合

m个样本,a个样本分类错误

  • 错误率(error rate) E = a / m:分类错误的样本数占样本总数的比例
  • 精度(accuracy):1 - a / m

把训练样本自身的一些特点当作了所有潜在样本都会具有的一般性质,会导致泛化性能下降。这种现象在机器学习中称为**“过拟合”(overfitting)“欠拟合”(underfitting)**指对训练样本的一般性质尚未学好。


评估方法

包含$m$个样例的数据集$D={(x_1,y_1), (x_2,y_2),…,(x_m,y_m)}$,如果既要训练,又要测试,需要把数据集$D$进行适当的处理,从中产生出训练集$S$测试集$T$。对数据集$D$的处理方法,有以下常见的几种。


留出法 (hold-out)

留出法直接将数据集$D$划分为两个互斥的集合。在$S$上训练出模型后,用$T$来评估其测试误差,作为对泛化误差的估计

训练/测试集的划分要尽可能保持数据分布的一致性,避免因数据划分过程引入额外的偏差而对最终结果产生影响,例如在分类任务中至少要保持样本的类别比例相似。如果从采样的角度来看待数据集的划分过程,则保留类别比例的采样方式通常称为分层采样(stratified sampling)。若$S$、$T$中样本类别比例差别很大,则误差估计将由于训练/测试数据分布的差异而产生偏差。

即使给定训练/测试集的样本比例后,仍然存在多种划分方式对初始数据集$D$进行分割。因此,单次使用留出法得到的估计结果往往不够稳定可靠。使用留出法,一般要采用若干次随机划分、重复进行实验评估后取平均值作为留出法的评估结果。

常见的做法是将大约${\frac{2}{3}}$~${\frac{4}{5}}$的样本用于训练,剩余样本用于测试。


交叉验证法 (cross validation)

交叉验证法先将数据集$D$划分为$k$个大小相似的互斥子集,每个子集$D_i$都尽可能保持数据的一致性,即从$D$中通过分层采样得到。然后,每次用$k-1$个子集的并集作为训练集,剩下的那个子集作为测试集,这样就可以获得$k$组训练/测试集,最终返回$k$个测试结果的平均值

交叉验证法评估结果的稳定性和保证性很大程度上取决于k的取值,因此交叉验证法通常又称为**$k$折交叉验证($k$-fold cross validation)。$k$最常用的取值是10,此时称为10折交叉验证**,其他常用的k值有5、20等。

与留出法相似,将数据集$D$划分为$k$个子集同样存在多种划分方式。为减小因样本划分不同而引入的差别,$k$折交叉验证通常要随机使用不同的划分重复$p$次,最终结果是这$p$次$k$折交叉验证结果的均值,常见的有10次10折交叉验证


自助法 (bootstrapping)

自助法将给定包含$m$个样本的数据集$D$进行采样,产生数据集$D^{‘}$:每次随机从D中挑选一个样本,将其拷贝放入$D^{‘}$,这个过程重复执行$m$次,就得到了数据集$D^{‘}$。

$D$中有一部分样本会多次出现,另一部分样本则不出现。样本在$m$次采样中始终不被采到的概率是$(1-\frac{1}{m})^m$,取极限:$\displaystyle\lim_{x\to\infty}(1-\frac{1}{m})^m={\frac{1}{e}}\approx 0.368$。即通过自助采样,初始数据$D$中约有36.8%的样本不会出现在采样数据集$D^{‘}$中。于是,可将$D^{‘}$用作训练集,剩余样本用作测试集。这样,实际评估的模型与期望评估的模型都使用$m$个训练样本,而仍有数据总量约1/3的、没在训练集中出现的样本用于测试。这样的测试结果,亦称**“包外估计”(out-of-bag estimate)**。

自助法在数据集较小、难以有效划分训练/测试集时很有用;自助集能从初始数据集中产生多个不同的训练集,这对集成学习等方法有很大好处

然而,自助法产生的数据改变了初始数据集的分布,会引入估计偏差。因此,在初始数据量足够时,留出法和交叉验证法更常用


调参(parameter tuning)与最终模型

大多数学习算法都有些参数需要设定,参数配置不同,学得模型的性能往往有显著差别。因此,在进行模型评估与选择时,还需要算法参数进行设定,这就是常说的调参

学习算法的很多参数在实数范围内取值,因此对每种参数配置都训练出模型是不可行的。常用的做法是,对每个参数选定一个范围和变化步长。例如,在$[0, 0.2]$范围内以0.05为步长,则实际要评估的候选参数数值有5个,最终从这5个候选值中产生选定值。显然,这样选定的参数值往往不是最佳值,但这是计算开销和性能估计之间进行折中的结果,通过这种折中,学习过程才变得可行

在研究对比不同算法的泛化性能时,我们用测试集上的判别效果来估计模型在实际使用时的泛化能力。而把训练数据另外划分为训练集和验证集(validation set),基于验证集上的性能来进行模型选择和调参


性能度量 (performance measure)

对学习器的泛性能进行评估,不仅需要有效可行的实验估计方法,还需要有衡量模型泛化能力的评价标准,这就是性能度量

回归任务最常用的性能度量是均方误差(mean squared error)

$$E(f; D)=\frac{1}{m}\sum_{i=1}^{m}(f(x_i)-y_i)^2$$

更一般地,对于数据分布D和概率密度函数p(·),均方误差可描述为:

$$E(f; D)=\int_{x\sim D}(f(x)-y)^2p(x)dx$$


错误率与精度

经验误差与过拟合中,提到了错误率和精度。这是分类任务中最常用的两种性能度量,既适用于二分类任务,也适用于多分类任务。

  • 错误率

    $$E(f;D)=\frac{1}{m}\sum_{i=1}^mⅡ(f(x_i)\ne y_i)$$

  • 精度

    $$acc(f;D)=\frac{1}{m}\sum_{i=1}^mⅡ(f(x_i)=y_i)=1-E(f;D)$$

更一般地,对于数据分布D和概率密度函数p(·),错误率和精度可分别描述为

  • $$E(f;D)=\int_{x\sim D}Ⅱ(f(x)\ne y)p(x)dx$$
  • $$acc(f;D)=\int_{x\sim D}Ⅱ(f(x)=y)p(x)dx=1-E(f;D)$$

查准率(Precision)、查全率(Recall)与F1

对于二分类问题,可将样例根据其真实类别与学习器预测类别的组合划分为真正例(True Positive)、假正例(False Positive)、真反例(True Negative)、假反例(False Negative)

  • 样例总数=$TP+FP+TN+FN$

  • 查准率P

    $$P=\frac{TP}{TP+FP}$$

  • 查全率R

    $$R=\frac{TP}{TP+FN}$$

  • 基于P和R的**调和平均(harmonic mean)**定义的F1

    $$\frac{1}{F1}=\frac{1}{2}(\frac{1}{P}+\frac{1}{R})$$

在一些应用中,对查准率和查全率的重视程度不同。例如,在商品推荐系统中,为了尽可能少打扰用户,更希望推荐内容确实是用户感兴趣的,此时查准率更重要;而在逃犯信息检索系统中, 更希望尽可能少漏掉逃犯,查全率更重要。F1度量的一般形式$F_\beta$,能表达出不同的偏好:

$$\frac{1}{F_\beta}=\frac{1}{1+\beta^2}(\frac{1}{P}+\frac{\beta^2}{R})$$


ROC与AUC

分类过程相当于以某个**截断点(cut point)**将样本分为两部分,前一部分判作正例,后一部分判作反例。

  • ROC(Receiver Operating Characteristic)

    受试者工作特征曲线,根据学习器的预测结果对样例进行排序,按此顺序逐个把样本作为正例进行预测,每次计算出两个重要量的值,分别作为横纵坐标作图,就得到了ROC曲线。

注释

// 单行注释

/*
多行注释
*/

/**
 * JavaDoc:文档注释
 * 
 * 
 */

标识符

类名变量名方法名都被称为标识符

  • 关键字不能作为标识符
  • 所有的标识符都必须以字母、$或下划线_开始;首字符之后还可以选择数字作为字符。
  • 标识符中不能使用#*-等字符
  • 标识符是大小写敏感

数据类型

  • 强类型语言

    要求变量的使用严格符合规定,变量都必须先定义再使用。Java就是强类型语言。

  • 弱类型语言

    JavaScript就是弱类型语言。


Java的数据类型分为两大类

基本类型(primitive type, 8个)

  • 数值类型

    • 整数类型

      整数类型 大小 表示范围
      byte 8位 -128($-2^7$) ~ 127($2^7-1$)
      short 16位 $(-2^{15})$~$(2^{15}-1)$
      int 32位 $(-2^{31})$~$(2^{31}-1)$
      long 64位 $(-2^{63})$~$(2^{63}-1)$
      • long类型的变量,赋值时需要在最后加一个L,便于和int类型区分。(不加L也能通过编译,但是可读性差小写的l辨识度低,可读性也差)
      • float类型的变量,赋值时必须在最后加一个F
    • 浮点数类型

      浮点类型 大小
      float 32位
      double 64位

      最好完全不要使用浮点数进行比较,因为浮点数是有限位的、离散的、有舍入误差的数据类型。

      那么银行类业务应该用什么数据类型表示钱?——数学工具类 BigDecimal

    • 字符类型

      char,16位,赋值使用单引号''字符串String不是关键字,而是一个类,赋值使用双引号""

    long num1 = 30L; 
    float num2 = 3.14F;
    char c = 'A';
    String str = "AS";

    JDK7的特性:赋值时,可以用下划线_分割过长的数字(整数&浮点数均可),提高可读性

  • 布尔类型 boolean

    1位,值为true/false,由所占空间大小可知,**实际是使用(1/0)**来表示。


引用类型(reference type)

    • String

      • 判断字符串是否相等

        str1.equals(str2)重写了继承自Object的方法

        而不是通过==来判断,因为String是引用类型中的==作用于对象实际比较的是地址

      •   String str = "test" // 实际等价于 String str = new String("test");
          
          	
          		**不用创建对象的写法可以看作Java为了方便的设计,并不意味着String类是和char一样的基本类型**。
        
        - 接口
        
        - 数组
        
        ---
        
        ### 数据类型扩展
        
        #### 不同进制整数的特殊表示方法
        
        - **二**进制:**0b**开头
        - **八**进制:**0**开头
        - **十六**进制:**0x**开头
        
        ```java
        int num1 = 0b101; // 0b开头,2进制
        int i = 10;
        int i2 = 010; // 0开头,8进制
        int i3 = 0x10F; // 0x开头,16进制

字符转换成数字后的输出

char c1 = 'a';
char c2 = '\u0061' // \u为转义字符 Unicode,后跟4位16进制的数字,有对应的字符
    
System.out.println(c1);
system.out.println((int)c1); // 强制类型转换
System.out。println(c2);

类型转换

由于Java是强类型语言,所以进行某些运算的时候,需要用到类型转换。


基本数据类型的优先级

// 低 --------------------------------------------> 高
byte, short, char, int, long, float, double
  • 虽然float是32位表示,long是64位表示,但浮点数优先级就是比整数高
  • 布尔类型不能进行强制转换
  • 对象类型不能转换为不相干的类型
  • 类型转换可能存在内存溢出或者精度问题

字符串连接符

String类型的操作数**+另外类型的操作数,会将其他类型的操作数都转换成Stirng类型**后再连接。

System.out.println("" + 2 + 3); // 输出23
System.out.println(2 + 3 + ""); // 输出5

变量、常量、作用域

变量 Variable

// 数据类型 变量名 = 值
type varName [=value] [{, varName[=value]}];
  • 可以使用逗号隔开来声明多个同类型的变量,但是不建议这样操作(后续查找、修改的操作会变得麻烦,可读性差)。

  • 变量的默认初始化

    • 基本数据类型:

      • 整数类型:0
      • 浮点数类型:0.0
      • 字符类型:’\u0000’
      • 布尔类型:false
    • 引用数据类型:

      默认初始化为空值null,如String。


作用域

  • 类变量:从属于类,使用关键字static修饰
  • 实例变量:从属于对象,需要创建出对象后,才能使用
  • 局部变量:位于方法中

e.g. 见Demo1.java文件


常量 Constant

  • 初始化后不能更改,使用关键字final修饰;

修饰符不存在先后顺序

e.g. 以下两种写法等效

public static final double PI = 3.14;
static public final double PI = 3.14;

变量和常量的命名规范

  • 变量

    • 首字母小写驼峰原则:monthSalary
    • 方法名的规范同变量名
    • 类名要求首字母大写和驼峰原则
  • 常量

    常量一般用大写字母和下划线命名,便于和变量区分,提高可读性;


Math类

很多运算,我们会使用一些工具类来操作。

  • pow(double a, double b)

    返回一个结果为$a^b$的double值


运算符

位运算符

位运算与普通的四则运算相比执行效率高很多,因为位运算是直接以计算机底层的运行机制为基础操作的。

  • &

  • |

  • ~ 取反

  • 移位运算符

    • << 左移

      每移1位,等效于*2

    • >> 右移

      每移1位,等效于/2


三元运算符x?y:z

如果x == true,结果为y,否则结果为z


包机制

为了更好地组织类,Java提供了包机制,用于区别类名的命名空间包的本质 == 文件夹

  • 一般利用公司域名倒置作为包名

    com.hunter

  • 包语句的语法格式

    package package1[.pkg2[.pkg3...]];

    必须是类文件中的首句

  • 为了能够使用某一个包的成员,需要在Java文件中使用import语句明确导入该包

    import package1[.pkg2...].(classname|*); // *是通配符,导入指定包下所有的类
  • 尽量不要让不同包下的类名重复

    同一个类文件中,无法使用位于不同包下相同类名的类(会报错)


JavaDoc生成文档

  • javadoc命令是用来生成API文档

  • 文档注释

    /**
    *
    * 
    */
    • 加在类上就是类注释
    • 加在方法上就是方法注释
  • 参数信息

    • @auther 作者名
    • @version 项目版本号
    • @since 指明需要最早使用的jdk版本
    • @param 参数名
    • @return 返回值情况
    • @throw 异常抛出情况

卸载JDK

  1. 删除Java的安装目录及其下所有文件
  2. 删除系统环境变量 - JAVA_HOME
  3. 删除系统环境变量Path下关于Java的目录
  4. 通过java -version命令,验证是否成功卸载(报错即成功卸载)

安装JDK

  1. 下载JDK 8
  2. 双击安装JDK
  3. 记住安装的路径

配置环境变量

  1. 我的电脑 - 右键 - 属性 - 高级系统设置 - 环境变量

  2. 系统变量 - 新建

    1. 变量名:JAVA_HOME
    2. 变量值:C:\Program Files\Java\jdk1.8.0_201
  3. 系统变量 - Path - 编辑 - 新建

    • 输入%JAVA_HOME%\bin%%的作用:引用路径
    • 输入%JAVA_HOME%\jre\bin

    确定,保存。

  4. 测试JDK是否安装成功

    打开命令行,输入java -version


Hello World 及 简单语法规则

  1. 创建一个java文件

    • 文件后缀名为.java
  2. 编写代码

    public class Hello {
        public static void main(String[] args) {
             System.out.print("Hello, World!");
        }
    }
  3. 编译java文件

    javac xxx.java,会生成一个class文件

  4. 运行class文件

    java xxx(不写文件类型后缀)

注意事项

  1. Java是严格区分大小写
  2. 文件名类名必须保持一致,并且首字母大写

Java程序运行机制


使用IDEA

什么是IDE

IDE(Integrated Development Environment,集成开发环境)


IDEA 使用技巧

0%