Java基础之《Java核心卷1》第4,5章

4. 对象和类

4.1 一些概念

  • 类和对象:类是对象的抽象,对象是类的具体实例;例如学生是一个类,学生中的张三就是一个对象
  • 对象和对象变量:参考:https://blog.csdn.net/a850661962/article/details/102899790
    当执行 People peo1 = new People(); 时,可以拆分为以下几步
    (1)创建对象:new People(),在堆中为新创建的对象分配内存,即真实的对象保存在堆内存中
    (2)声明变量:People peo1,在栈中为新声明的对象变量分配内空间,即对象变量保存在占栈内存中。
    (3)变量赋值:peo1 <= 新建的对象,将对象在堆中的首地址赋值给对象变量
    !注意:未引用对象的对象变量不能使用类中的方法

4.2 Date类和LocalDate类

Date

Date类表示一个时间点,如:“December 31,2020,23:59:59 GMT”

  • 示例:获取当前时间

    import java.util.Date;
    Date date = new Date();
    System.out.println(date);
    System.out.println(date.toString());
    
  • 常用API

    public Date();//根据当前默认毫秒值创建日期对象
    public Date(long date);//根据给定的毫秒值创建日期对象
    public long getTime();//日期转毫秒值
    public void setTime(long time);//毫秒值转日期,可以用 System.currentTimeMillis();获取当前日期毫秒数
    

LocalDate

LocalDate类会使用日历表示法表示日期

  • 示例

    import java.time.LocalDate;
    LocalDate localTime = LocalDate.now();//获得当前日期,以2022-11-8的形式
    System.out.println(localTime.getDayOfWeek());//获取星期几
    
  • 常用API

    static LocalTime now();//构造一个表示当前日期的对象
    static LocalTime of(int year, int month, int day);//构造一个表示给定日期的对象
    int getYear();//获取年
    int getMonthValue();//获取月
    int getDayOfMonth();//获取日
    DayOfWeek getDayOfWeek();//获取星期几
    LocalDate plusDays(int n);//生成当前日期之后n天的日期
    LocalDate minusDays(int n);//生成当前日期之前n天的日期
    

打印日历表

import java.time.DayOfWeek;
import java.time.LocalDate;
public class Test{
    public static void main(String[] args){
        LocalDate date = LocalDate.now();
        int month = date.getMonthValue();
        int today = date.getDayOfMonth();
        date = date.minusDays(today - 1); //初始设置为本月的第1天
        DayOfWeek weekday = date.getDayOfWeek() ;
        int value = weekday.getValue();//1=Monday,...7=Sunday
        System.out.println("Mon Tue Wed Thu Fri Sat Sun") ;
        for (int i = 1; i < value; i++)
            System.out.print(" ");
        while (date.getMonthValue() == month) {
            System.out.printf("%3d", date.getDayOfMonth());//获取日
            if (date.getDayOfMonth() == today)//如果是今天,就输出 *
                System.out.print("*");
            else
                System.out.print(" ");//否则输出空格
            date = date.plusDays(1);//日期向后推一天
            if (date.getDayOfWeek().getValue() == 1) System.out.println();//如果是星期一,就换行
        }
        if (date.getDayOfWeek().getValue() == 1) System.out.println();
    }
}

访问对象但不更改对象的方法有时称为访问器方法,如LocalData.plusDays(1000);原对象不变,而是生成了一个新的对象
C++中,带有const后缀的就是访问器方法
如果需要返回一个可变数据域的拷贝, 应该使用clone

4.3 用户自定义类

当编译Test.java时,会将它引用的其他源文件一同编译,并生成.class文件

构造器

  • 构造器与类同名(C++中的构造函数),如 public Test(){}
  • 每个类可以有一个以上的构造器
  • 构造器可以有0 个、1 个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着new操作一起调用

所有的java对象都是在堆中构造的

隐式参数和显式参数:隐式参数是没有显示的第1个参数 this,指向本对象;显式参数是函数被声明了的可传递的参数

在java中,所有的方法都必须在类的内部定义
C++一般在外部定义,在内定义的是内联函数

final:大都应用于基本(primitive ) 类型域, 或不可变(immutable) 类的域,如String就是不可变域

4.4 静态域和静态方法

静态域(静态变量)

class Employee{
    private static int nextId = 1;
    private int id;
}

静态域nextId为所有对象共享,例如100个对象,就有100个id,但只有1个nextId;
任意对象对nextId的更改都会被其他对象获取(对所有的Employee来说相当于一个全局变量)

静态常量

静态变量使用得比较少, 但静态常量却使用得比较,如

public class Math{
    ...
	public static final double PI = 3.14159265358979323846;
    ...
}
//通过Math.PI即可访问,注意前面的静态域(静态变量)不能通过类名.变量名的方式访问

单纯static定义的静态域不建议使用public,但static final定义的静态常量却可以用public

System.out 中的out就是一个静态常量,但依然可以通过setOut()来修改它。这是因为setOut是一个本地方法,不是java编写的,可以绕过java的存取控制机制

静态方法

如 Math.pow(x, a),在运算时,不使用任何的Math对象,因而也没有隐式参数 this

  • 静态方法不能访问普通实例域,如上面的id实例域,因为它不能操作对象
    静态方法是在类加载是初始化的,而实例域要等对象创建才会初始化,如果允许了这种方法,就可能出现静态方法访问不存在的实例域的情况
  • 但是静态方法可以访问静态域(适用于一些没有自己数据的类,如为运算而创建的Math类)

只有静态方法或静态常量能通过 类名.方法名 这样的方式访问;其他的都需要 对象名.方法名 进行访问

静态工厂方法

工厂方法,例如LocalDate.now(),NumberFormat.getCurrencylnstance()等,不使用构造器构造对象,而使用静态工厂方法来构造
参考:https://blog.csdn.net/slw20010213/article/details/121771785

  • 普通方法获取类实例
    Student student = new Student()
    
  • 静态工厂方法
    Fragment fragment = MyFragment.newIntance();
    Calendar calendar = Calendar.getInstance();
    Integer number = Integer.valueOf("3");
    //用一个静态方法来对外提供自身实例的方法,即静态工厂方法
    

使用静态工厂方法的优点

  • 更容易区分
    使用构造函数new一个实例时,构造函数必须同类名相同,如果此时有多个构造函数,将变得难以记忆。静态工厂方法允许给不同类型的构造方式取有意义的名字,例如 valueOf、newInstance
  • 不必每次都创建新对象
    静态工厂方法不一定创造新对象(取决于具体实现,可以返回已有对象)。单例模式大多使用静态工厂方法实现
  • 可以返回子类
    构造器只能返回自身,但通过自定义静态工厂方法,可以使其返回子类

4.5 方法参数

有两种调用类型:按值调用和按引用调用
基本数据类型:按值调用;形参是实参的拷贝,不会影响实参的值
对象引用:也是按值调用,只是这个值是同一对象的引用(类似地址拷贝了一份);形参是实参的拷贝,两者都是同一个对象的引用,形参操作会影响实参

  • java中对象并不是引用调用

    public static void swap(Student stu1, Student stu2){
        Student temp = stu1;
        stu1 = stu2;
        stu2 = temp;
    }
    

    以上函数就无法改变原函数中stu1和stu2中的状态;因为传递的是引用的拷贝,交换的是被拷贝的引用,和原引用无关

4.6 对象构造

重载:方法名相同,但参数不同(仅返回值不同不能构成重载)

默认域初始化:数值为0,布尔型为false,对象引用为null;

无参构造:系统会提供一个默认的无参构造,使用默认域初始化;但如果自定义了有参构造,系统就不提供默认的无参构造,此时使用无参构造会报错

显式域初始化:java允许在创建类时为各个域设置默认值,C++不允许,这是因为java对象没有子对象,只有指向其他对象的指针

this调用另一个构造器:

//this表示当前对象的引用
public Employee(double s){
	this("Employee #" + nextId, s);//this将调用一个新的构造器,Employee(String, double);
	nextId++;
}

C++中,一个构造器不能调用另一个构造器

初始化块:

class Employee{
    private static int nextId;
    private int id;
    private String name;
    private double salary;
    //每个Employee对象都会执行如下代码,以完成初始化
    {
        id = nextld;
        nextld++;
    }
    public Employee(String n, double s){
        name = n;
    	salary = s;
    }
    public Employee(){
        name = "";
        salary = 0;
    }
}

类中变量初始化的三种方式:构造函数、显式域初始化、初始化块

执行顺序:构造器、初始化块或初始化语句、第二个构造器…

对象析构与finalize:java有自动垃圾回收机制,不像C++那样需要自行定义析构函数;但当对象使用了内存之外的其他资源,就需要finalize方法对其进行回收

4.7 包

推荐将公司的因特网域名的逆序作为包名

从编译器的角度来看, 嵌套的包之间没有任何关系。例如,java.util 包与java.util.jar 包毫无关系。每一个都拥有独立的类集合

一个类可以使用所属包中的所有类, 以及其他包中的公有类

在包中定位类是编译器(compiler)的工作。类文件中的字节码使用完整的包名来引用其他类

import和C++中的#include不同,java的package和import相当于C++的namespace和using命令

类和包:要想将类放入包中,必须将包名放在源文件的开头,例如

package pack;//IDEA以src为当前目录,包名引用只需要从src后开始

即便包名位置写错,也能通过编译,但运行时可能会出现问题

包的作用域:

  • public定义的部分可以被任意类使用
  • private定义的部分只能被定义它们的类使用
  • 未指定public或private,可以被当前包内的所有方法访问

从1.2 版开始, JDK 的实现者修改了类加载器, 明确地禁止加载用户自定义的、包名以“ java.”开始的类

public、private、protected

  • private:仅对本类可见
  • public:对所有类可见
  • protected:对本包和所有子类可见
  • 默认:不加修饰符,对本包可见

4.8 类路径

虚拟机定位类(寻找.class文件):

  • 首先要查看存储在jre/lib和jre/lib/ext 目录下的归档文件中所存放的系统类文件
  • 然后再查看类路径(IDEA 标蓝的文件夹就是类路径)

编译器定位类:引用了一个类,而没有指出这个类所在的包

  • 首先查找包含这个类的包,并询查所有的import指令, 确定其中是否包含了被引用的类

设置类路径

//WINDOWS
set CLASSPATH=c:\classdir;.;c:\archives\archive.jar//"."表示当前目录,多个路径用;分开
//Linux/Unix
export CLASSPATH=/home/user/classdir:.:/ home/user/archives/archive.jar//多个路径用:分开

4.9 文档注释 JavaDoc

创建JavaDoc文档

方法1:命令行

javadoc -d filename -author -version -encoding UTF-8 Test.java
//使用@的注释需要特别指明,否则不显示

方法2:IDEA

tools - generate JavaDoc

在这里插入图片描述

默认@开头,如@author,不显示

注释标记

  • 普通文本

使用/** */,可以使用html语法

  • 方法注释
@param 变量描述,一个方法的所有@param标记必须放在一起
@return 返回值描述
@throws 表示这个方法可能会抛出异常
  • 通用注释
@author 每个@author对应一个作者
@version 版本描述
@since	始于,如@since version 1.7.1
@deprecated 不再使用标志,可以给出替代方法,如 @deprecated Use <code> setVisible(true)</code> instead
@see 插入一个超链接,如 @see com.horstraann.corejava.Employee#raiseSalary(double),raiseSalary的超链接
@link@see类似,但可以在任意位置插入,如{@link package.class#feature label}

5. 继承

5.1 类、超类、子类

基础

java中所有的继承都是公有继承,C++中还有私有继承和保护继承

超类和子类:超类又称为父类、基类;子类又称为派生类或孩子类

!!子类不能直接访问父类的私有域

  • 重写:示例,重写父类方法,super的使用

    class Father{
        private int salary = 100;
        int getSalary(){
            return salary;
        }
    }
    class Child extends Father{
        private int bonus = 5;
        int getSalary(){
            //return salary+bonus;//报错,子类不能直接访问父类的私有域,非私有域可以直接访问
            //return getSalary()+bonus;//报错,父类和子类都有getSalary,会反复调用自己
            return super.getSalary()+bonus;//成功
        }
    }
    

    this是一个对象的引用,super不是,super只是一个指示编译器调用父类的关键字

    子类不能删除父类中的任何域和方法

  • 子类构造器:通过super调用父类构造器

    public Manager(String name, double salary, int year, int month, int day){
        super(name, salary, year , month, day) ;
        bonus = 0;
    }
    

    super 调用父类构造器必须在子类构造器的第一条语句
    如果子类构造器没有显式调用父类构造器,将自动地调用父类的无参构造;如果找不到无参构造,则报错

java允许多继承,因而有继承链,C++中不允许多继承
注意:多继承是指A extends B, B extends C,这样A就继承了B和C;不能是A extends B,C

多态

在Java 程序设计语言中, 对象变量是多态的。一个Employee变量既可以引用一个Employee 类对象, 也可以引用一个Employee 类的任何一个子类的对象

父类引用子类对象,则这个引用不能使用子类独有的方法,相当于一个父类对象

不能将一个超类赋给子类:对象的大小是由 new 关键字在堆上开辟的大小,可以使用哪些方法是由对象变量类型决定的,将较小的空间的超类分给子类,访问子类的某个变量时,可能发生内存错误

  • 方法调用
    参考:https://blog.csdn.net/zcxwww/article/details/51303928
    为方便调用,虚拟机会为每个类生成方法表,以供调用

    • 静态绑定
      如果是private、static、final定义的方法,或者构造器,这种调用方式称为静态绑定

      public class Father {
      	public static final void f1(){
          	System.out.println("Father-f1()");
      	}
      }
      public class StaticCall {
          public static void main(String[] args) {
              Father.f1();
          }
      }
      

      虚拟机将f1()这个方法的符号引用通过invokespecial指令放入常量池中,然后在Father类所在的方法区中找到f1()方法的直接地址,并将这个直接地址记录到常量池中的常量表中。这个过程叫做常量池解析。以后再次调用Father.f1()是,将直接找到f1方法的字节码

      这种在编译阶段就知道要调用哪个方法的方式(知道符号引用(方法名)),叫做动态绑定机制

    • 动态绑定
      编译的时候该方法不与所在类绑定,编译器此时依然不知道对象的类型。java里实现动态绑定的是JVM.

      Father fa=new Son();
      fa.say();
      

      由于fa的类型是Father,所以一开始还是会到Father类的方法表里去找say()方法,获得它在方法表里的位置是第N项,如果找不到就直接编译报错;然后根据堆里new的具体实例的类型(这里是Son),去Son里面的第N项取出say()方法在内存里的地址,然后执行

    方法的名字和参数列表,被称为方法的签名

阻止继承 final

public final class Executive extends Manager

可以为父类的方法添加final关键字,子类就无法重写这样的方法
final类中的方法默认为final,但域不会
final定义的类无法被继承,final定义的方法无法被修改

在早期java中,如果一个方法如果没有被覆盖或者很短,编译器就能够对它进行优化处理, 这个过程为称为内联 (inlining)
现在即时编译器将很简短、被频繁调用且没有真正地被覆盖的方法定为内联,如果之后方法被覆盖,优化器再取消内联

类型转换

  • instanceof

    //实行类型转换时(主要是类),建议使用instanceof检测是否能够进行转换
    if(var instanceof Student){...}//查看var对象是否能够转为Student类
    

    (子类 instance 父类),返回true,即便是子类的子类也是true
    (父类 instance 子类),返回false

    java的类型转换类似于C++的dynamic_cast,但是不同在于转换失败时java不会生成一个null对象,而是抛出异常

抽象类

包含一个或多个抽象方法的类必须被声明为abstract
抽象类中可以定义非抽象方法,但不推荐这样使用
即使类中不含抽象方法,也可以声明为抽象类

public abstract class Person{
    private String name;
    public abstract String getDescription();
    public String getName(){
        return name;
    }
}

子类继承抽象类必须重写超类中的所有抽象方法

class Student extends  Person{
    @Override
    public String getDescription() {
        return null;
    }
    @Override
    public String getAge() {
        return null;
    }
}

抽象类不能被实例化,但可以引用子类对象

Person per1 = new Person();//报错
Person per2 = new Student();//通过

5.2 Object超类

java中所有的类都是Object超类的子类,因此可以使用Object引用所有类型的对象

Object obj = new Person();

在java中只有基本类型不是对象,所有的数组类型(包括对象数组)都扩展了Object类,如

Student[] stus = new Student[10];
Object obj = stus;
obj = new int[10];
  • equals
    在子类中定义equals方法时,首先调用super.equals(other),之后再逐一比较子类特有的域是否相等

    对于数组类型,可以使用静态的Arrays.equals()方法检测

hashCode 散列码

hashode() 方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址

int[] a =  new int[10];
System.out.println(a);//输出:[I@1b6d3586
System.out.printf("%#x\n",a.hashCode());//输出:0x1b6d3586

重写equals方法时,就必须重新定义hashCode方法;因为equals的条件之一是其hashCode相等
String类就重写了hashCode方法,因此得到的并非是地址

  • String

    //String定义hashCode的方式
    int hash = 0;
    for (int i = 0; i < length(); i++)
    	hash = 31 * hash + charAt(i);
    
    String s = "Ok";
    StringBuilder sb = new StringBuilder(s);
    System.out.println(s.hashCode() + " " + sb.hashCode());//输出:2556 460141958
    String t = new String("Ok") ;
    StringBuilder tb = new StringBuilder(t);
    System.out.println(t .hashCode() + " " + tb.hashCode());//输出:2556 1163157884
    //s和t的散列码相同,因为它们是按照String计算规则得到的
    //sb和tb的散列码不同,因为在StringBuffer类中没有定义hashCode,因此返回的是对象存储地址
    if(sb.equals(tb)) System.out.println("equals");//发现并不相等,因为equals要求它们的hashCode也相同
    
  • 自定义类的hasCode

    //调用Object中的hash方法,并传参
    public int hashCodeO{
    	return Objects.hash(name, salary, hireDay) ;
    }
    //Equals与hashCode 的定义必须一致,如果equals返回true,那么hashCode也应当返回true
    //如果比较的仅是id,那么这里的hash传递的参数就只有(id)
    

toString 方法

大部分遵循:类名+方括号+域值

Object中的toString会打印输出对象的类名和散列码

例如自定义的Employee类的toString实现方法:

//注意要定义为public
public String toString(){
    return getClass().getName()
    + "[name=" + name
    + ",salary=" + salary
    + ",hireDay=" + hireDay
    + "]";
}
//如果是子类,还可以
public String toString(){//注意要定义为public
    return super.toString()
    + "[bonus=" + bonus
    + "]";
}

数组继承了Object类,使用toString只能打印地址,应当使用Arrays.toString,多维数组使用deepToString

int[] a = {1,2,3};
System.out.println(a.toString());//输出:[I@1b6d3586
System.out.println(Arrays.toString(a));//输出:[1, 2, 3]
  • API

    Class getClass();//返回包含对象信息的类对象。Java提供了类运行时的描述,它的内容被封装在Class类中
    boolean equals(Object otherObject);//如果两个对象指向同一块存储区域,方法返回true,在自定义的类中应该覆盖这个方法
    String toString( );//返冋描述该对象值的字符串。在自定义的类中应该覆盖这个方法
    
    String getName();//返回这个类的名字
    Class getSuperclass( );//以Class对象的形式返回这个类的超类信息
    

5.3 泛型数组列表 ArrayList

java中可以通过new,在运行时动态定义数组大小
但数组大小一旦确定,就不好更改,可以使用java中的 ArrayList 替代,它具有自动调节数组容量的功能

ArrayList 是一个采用类型参数( type parameter ) 的泛型类( generic class )
ArrayList的<>不能使用 int,而是用 Integer 等代替

ArrayList<Employee> staff = new ArrayList<Employee>();//定义
ArrayList<Employee> staff = new ArrayList();//Java SE 7,可以省略掉右边的<Employee>
//Java SE 5.0 以前的版本没有提供泛型类, 而是有一个ArrayList类,如果一定要使用老版本的Java, 则需要将所有的后缀<...> 删掉
//老版本中会使用Vector类,但实际ArrayList更有效
staff.add(new Employee("Harry Hacker", ...));//添加一个对象
staff.ensureCapacity(l00);//这个方法调用将分配一个包含100个对象的内部数组。然后调用100次add,而不用重新分配空间
ArrayList<Employee> staff = new ArrayList<>(100);//初始化容量为100
staff.size();//返回包含的实际元素数目

Employee employees = new Employee[100]; employees被分配空间,但employees[0],employees[1] … 为null;但employees.length为100
employees = new ArrayList<>(100)只有add等操作之后才会有内存空间,且employees.size()为0

一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法,回收多余的存储空间

ArrayList类似于C++中的vector,但java中没有重载[],所以ArrayList必须显式调用
另外类似于 a=b,C++中是开辟一个新的vector,然后将值复制过去,而Java中只是将两个变量引向同一个数组列表

  • API

    //java.util.ArrayList<E> 1.2
    ArrayList<E> ();//构造一个空数组列表
    ArrayList<E> (int initialCapacity);//用指定容量构造一个空数组列表
    boolean add(E obj);//在数组列表的尾端添加一个元素。永远返回true
    int size();//返回存储在数组列表中的当前元素数量(这个值将小于或等于数组列表的容量。)
    void ensureCapacity(int capacity);//即便超过capacity也可以继续add,capacity是避免每次add都分配一次空间
    void trimToSize();//将数组列表的存储容量削减到当前尺寸。
    
    //java.util.ArrayList<T> 1.2
    void set(int index, E obj);//覆盖index位置的原有内容(必须index位置已存在对象才能set)
    E get(int index);//获得指定位置的元素值,必须介于0 ~ size()-1 之间
    void add(int index, E obj);//向后移动元素,以便插入元素,index是插入位置
    E removed(int index);//删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回
    
  • ArrayList的访问
    需要借助于get,且由于get返回的都是Object类对象,还需要进行强制类型转换,如

    Employee e = (Employee)staff.get(i);
    

    如果插入删除等操作比较多,且size比较大,应该考虑使用链表

    • 转换为数组

      ArrayList<X> list = new ArrayList<>();
      ...
      X[] a = new X[list.size()];
      list.toArray(a);
      //注意X不能是基本类型,int等可以用Integer代替
      
    • for each

      for(Employee e : staff)
      	...
      
  • 类型化和原始数组列表的兼容性
    原始数组列表不需要<>指定类型,因而可以将任意类型的对象添加到数组列表中,这样是不安全的

    • 可以将类型化数组列表赋给原始数组列表,不需要进行任何类型转换;
      可以将任意类型给原始数组列表,尽管可能不报错或警告,但不安全

    • 将原始数组列表赋给类型化数组列表,即便使用了类型转换,也将得到一个警告信息

      ArrayList<Employee> result = (ArrayList<Employee>)employeeDB.find(query) ;
      // yields another warning
      
    • 编译器在对类型转换进行检査之后, 如果没有发现违反规则的现象, 就将所有的类型化数组列表转换成原始ArrayList 对象
      因此, 类型转换( ArrayList ) 和( ArrayList< Employee > ) 将执行相同的运行时检查

    • @SuppressWamings(“unchecked”)

      //一旦能确保不会造成严重的后果,可以用该标注来标记这个变量能够接受类型转换
      @SuppressWarnings("unchecked") ArrayList<Employee> result =
      (ArrayList<Employee>)employeeDB.find(query);// yields another warning
      

5.4 对象包装器和自动装箱

  • wrapper
    所有的基本类型都有一个与之对应的类,这些类称为包装器(wrapper),如:Integer、Long、Float、Double、Short、Byte、Character 、Void 和Boolean,前6个派生于公共超类Number

    对象包装器类不可变,且由final修饰不可继承

    ArrayList<>中的<>不能是基本类型,这时候就需要用到对象包装器;
    但ArrayList的效率远远低于int[]

  • 自动装箱(autoboxing)
    list.add(3)会自动变换为list.add(Integer.valueOf(3)),这种变换就是自动装箱
    而int n = list.get(i)会自动变化为int n = list.get(i).intValue(),这称为自动拆箱

    不仅是在ArrayList中,在普通的算术运算符中也会自动装箱和拆箱

  • 相等的比较
    (1)==运算符

    Integer a = 1000;
    Integer b = 1000;
    if(a == b) System.out.println("==");//不输出,即a和b不相等
    

    == 比较的是对象是否指向同一块内存区域,上述语句一般是不成立的,但java中它们有可能成立,为了避免这种不确定性,应该使用equals

    (2)equals

      public class Test {
         public static void main(String[] args){
             Integer a = 1000;
             Integer b = 1000;
             System.out.println(a==b);//输出:false
             Integer c = 100;
             Integer d = 100;
             System.out.println(c==d);//输出:true
         }
      }
      //-128到127的数是放在数字常量池中的,在编译的时候会直接指向常量池中的数字,不会创建新对象,因此相等
      //不在这个范围,每次需要去堆中新建对象
    
  • 注意事项
    自动装箱要求boolean,byte,char ≤ \leq 127,short和int范围在-128到127之间的被包装在固定对象中,因此a=b=100,则==成立
    包装器引用可以为null,自动装箱可能抛出NullPointerException异常;
    Integer和Double混用,Integer值就会先拆箱,提升为double,再装箱为Double
    装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成字节码时插入必要的方法调用,虚拟机只是执行这些字节码
    Integer这类包装器是不可变的,因此不能使用这些包装器类创建修改数值参数的方法;需要使用IntHolder、BooleanHolder等

  • API

    //java.lang.Integer 1.0
    int intValue();//以int的形式返回Integer对象的值(在Number类中覆盖了intValue方法)
    static String toString(int i);//以一个新String对象的形式返回给定数值i的十进制表示
    static String toString(int i ,int radix);//返回数值i的基于给定radix参数进制的表示
    static int parseInt(String s);//返回字符串s表示的整型数值,给定字符串表示的是十进制的整数
    static int parseInt(String s,int radix);//返回字符串s表示的整型数值,给定字符串表示的是radix进制的整数
    static Integer valueOf(String s);//返回用s表示的整型数值进行初始化后的一个新Integer对象,给定字符串表示的是十进制
    Static Integer value Of(String s, int radix);//给定字符串表示的是radix进制
    
    //java.text.NumberFormat 1.1
    Number parse(String s);//返回数字值,假设给定的String表示了一个数值
    

5.5 可变参数

在Java SE 5.0 以前的版本中, 每个Java 方法都有固定数量的参数

public class PrintStream{
	public PrintStream printf(String fmt, Object... args) { return format(fmt, args);}
}

上述代码定义printf函数,它拥有可变参数
… 是Java代码的一部分,它表明这个方法可以接收任意数量的对象
实际上,printf 方法接收两个参数, 一个是格式字符串, 另一个是Object[]数组, 其中保存着所有的参数

  • 示例:求最大值

    public static double max(double... values){
        double largest = Double.NEGATIVE_INFINITY;//负无穷大
        for(double v:values) if(v>largest) largest = v;
        return largest;
    }
    //调用
    double m = max(3.1, 40.4, -5);
    //也可以用数组的方式调用:允许将一个数组传递给可变参数方法的最后一个参数,可变参数定义在最后
    double[] a = {3.1, 40.4, -5};
    double m = max(a);
    

5.6 枚举类

枚举其实是定义的一个类,所有的枚举都是enum的子类
枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数
适用场景:一周是七天,性别是男女,当一个变量有几种固定可能的取值时,就可以将它定义为枚举类型

  • 枚举类示例1

    enum Season{spring, summer, autumn, winter;}
    public class Test{
        public static void main(String[] args){
            System.out.println("请输入季节"+"\n"+"1、春天"+"\n"+"2、夏天"+"\n"+"3、秋天"+"\n"+"4、冬天");
            Scanner in = new Scanner(System.in);
            int select = in.nextInt();
            Season season = null;
            if(select == 3) season = Season.autumn;
            System.out.println(season);
        }
    }
    
  • 枚举类示例2

    enum Size{
        SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
        private String abbreviation;
        //构造函数的参数名称abbreviation决定了前面的SAMLL("S")中的参数含义
        private Size(String abbreviation) { this.abbreviation = abbreviation;}
        public String getAbbreviation() { return abbreviation; }
    }
    public class Test{
        public static void main(String[] args){
            String str = "LARGE";
            Size size = Enum.valueOf(Size.class, str);
            System.out.println(size);//输出:LARGE
            System.out.println(size.getAbbreviation());//输出:L
        }
    }
    
  • API

    //java.Iang.Enum <E> 5.0
    static Enum valueOf(Class enumClass, String name);//返回指定名字、给定类的枚举常量
    String toString();//返回枚举常量名
    int ordinal();//返回枚举常量在enum声明中的位置,位置从0开始计数
    int compareTo(E other);//如果枚举常量出现在Other之前,则返回一个负值;如果this=other,则返回0; 否则,返回正值
    

5.7 反射

一般是通过.java编译成.class,然后再运行.class文件
但有的时候,没有.java文件,即在程序运行过程中自动生成新的.class文件,这就是反射的应用场景

反射基础

反射可以在运行时获取一个类的所有信息
在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识
这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行
保存这些信息的类被称为Class,使用Object中的 getClass() 获取Class类型实例

反射最重要的内容:检查类的结构
不建议过多地使用反射,因为反射是很脆弱的,只有在运行时才能发现错误并导致异常

  • 获取Class的三种方法

    //1. getClass():通过对象获取类名
    //final Class getClass();//获取Class类
    e.getClass().getName();//获取类名,包括包名
    //2. Class.forName(),静态获取类名,类名运行时动态赋予时可用;需要异常处理
    //需要抛出ClassNotFoundException异常
    String className = "java.util.Random";
    Class cl = Class.forName(className) ;
    System.out.println(cl.getName());
    //3. class 通过类获取类名
    Class cl1 = Random.class;
    Class cl2 = int.class;//int不是类,但int.class是一个Class类型的对象
    Class cl3 = Double[].class;
    

    Class类实际上是一个泛型类,例如Employee.class 的类型是Class
    但在大多数实际问题中, 可以忽略类型参数, 而使用原始的Class 类

    虚拟机为每个类型管理一个Class 对象

  • 比较:== 和 newInstance

    //1. ==运算符:符实现两个类对象比较的操作
    if(e.getClass() == Employee.class) ...
    //2. newInstance:可以用来动态地创建一个类的示实例
    e.getClass().newInstance();//创建了一个与e具有相同类类型的实例
    
    String s = "java.util.Random";
    Object m = Class.forName(s).newlnstance();//创建了一个Random对象
    
  • API

    Class getClass();//返回Class
    Class getSuperClass();//返回超类Class
    //java.lang.Class 1.0
    static Class forName(String className);//返回描述类名为className 的Class 对象
    Object newInstance();//返回这个类的一个新实例。
    
    //java.lang.reflect.Constructor 1.1
    Object newInstance(Object[] args);//构造一个这个构造器所属类的新实例
    

Field Method Constructor

java.lang.reflect 包中有三个类Field、Method 和Constructor 分别用于描述类的域、方法和构造器

  • getModifiers:3个类都有,它将返回一个整型数值, 用不同的位开关描述public 和static 这样的修饰符使用状况
    Class、Field、Method、Constructor都有 getModifiers() 方法

    System.out.println(Class.forName("java.lang.String").getModifiers());//输出:17
    System.out.println(Modifier.toString(17));//参数是 int mod,这里输出:public final
    

    java.lang.reflect 包中的Modifier类的静态方法分析getModifiers 返回的整型数值,例如, 可以使用Modifier 类中的isPublic、isPrivate 或isFinal判断方法或构造器是否是public、private 或final
    还可以利用Modifier.toString 方法将修饰符打印出来。

  • Modifier类:可以用静态方法对 getModifiers 返回的整型数值解析;

    //常用方法
    Modifier.isPublic(17);//返回true;这里的17一般通过getModifiers()获取
    Modifier.isFinal(17);//返回true
    Modifier.isPrivate(17);//false
    Modifier.isProtected(17);//false
    Modifier.toString(17);//返回public final
    
  • Constructor类:描述构造器的类

    /**
    * 打印所有构造器
    * 关键:getConstructors, getName, getModifiers, getParameterTypes
    */
    Class cl = Class.forName("java.lang.String");
    Constructor[] constructors = cl.getConstructors();//获取所有的构造器
    for(Constructor c:constructors){
        String name = c.getName();//输出:java.lang.String
        System.out.print("  ");
        String modifiers = Modifier.toString(c.getModifiers());
        if(modifiers.length()>0) System.out.print(modifiers + " ");//输出:public
        System.out.print(name + "(");
        //打印参数列表,如果有类似[B的,[表示一维数组,B表示byte,C表示char
        Class[] paramTypes = c.getParameterTypes();
        for(int j=0; j<paramTypes.length; ++j){
            if(j>0) System.out.print(", ");
            System.out.print(paramTypes[j].getName());
        }
        System.out.println(");");
    }
    
  • Method类:描述方法的类

    /**
    * 打印所有方法
    * 关键:getMethods, getReturnType, getName, getModifiers, getParameterTypes
    */
    Class cl = Class.forName("java.lang.String");
    Method[] methods = cl.getMethods();
    for(Method m:methods){
        Class retType = m.getReturnType();
        String name = m.getName();
        System.out.print("  ");
        String modifiers = Modifier.toString(m.getModifiers());
        if(modifiers.length()>0) System.out.print(modifiers + " ");
        System.out.print(retType.getName()+" "+name+"(");
        //打印参数
        Class[] paramTypes = m.getParameterTypes();
        for(int j=0; j<paramTypes.length; ++j){
            if(j>0) System.out.print(", ");
            System.out.print(paramTypes[j].getName());
        }
        System.out.println(");");
    }
    
  • Field类:描述域的类

    /**
    * 打印所有方法
    * 关键:getDeclaredFields, getType, getName, getModifiers
    */
    Class cl = Class.forName("java.lang.String");
    Field[] fields = cl.getDeclaredFields();
    for(Field f:fields){
        Class type = f.getType();//这一个获取的域类型的Class
        String name = f.getName();//这一个获取的域名
        System.out.print("  ");
        String modifiers = Modifier.toString(f.getModifiers());
        if(modifiers.length()>0) System.out.print(modifiers+" ");
        System.out.println(type.getName()+" "+name+";");
    }
    
  • 示例

    /**
    * 控制台输入一个类,打印它的构造器、方法、域
    */
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.lang.reflect.Modifier;
    import java.util.Scanner;
    
    public class Test{
        public static void main(String[] args) throws Exception{
            //1. 输入要查看的类名
            String name;
            Scanner in = new Scanner(System.in);
            System.out.println("Enter Class name (e.g. java.lang.String): ");
            name = in.next();
            try{
                //2. 获取Class基本信息
                Class cl = Class.forName(name);//获取类的Class对象
                Class supercl = cl.getSuperclass();//获取超类的Class对象
                String modifiers = Modifier.toString(cl.getModifiers());//Modifier:描述修饰符使用情况,输出:public final
                if(modifiers.length()>0) System.out.print(modifiers + " ");
                System.out.print("Class " + name);//输出:Class java.lang.String
                if(supercl != null && supercl != Object.class)//获得超类Class
                    System.out.print(" extends " + supercl.getName());
                System.out.print("\n{\n");
                //3. 打印 Constructor
                printConstructots(cl);//和前面的代码一样的
                System.out.println();
                //4. 打印方法 Method
                printMethods(cl);//和前面的代码一样的
                System.out.println();
                //5. 打印域:Field
                printFields(cl);//和前面的代码一样的
                System.out.println("}");
    
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
  • API

    //java.lang.Class 1.0
    Field[] getFields();//1.1,返回一个包含Field对象的数组,这些对象记录了这个类或其超类的公有域(public)
    Filed[] getDeclaredFields();//1.1,返回一个包含Field对象的数组,但不包含超类域,包含本类的所有域(包括private)
    Method[] getMethods();//1.1,返回自己和超类的所有公用方法(public)
    Method[] getDeclareMethods();//1.1,返回本类中所有的方法,包括private,但不包括超类的任何方法
    Constructor[] getConstructors();//1.1,仅返回本类中的public构造器
    Constructor[] getDeclaredConstructors();//1.1,返回本类中的全部构造器
    
    //通用:java.lang.reflect.Field 1.1 和 java.lang.reflect.Method 1.1 和 java.lang.reflect.Constructor 1.1
    Class getDeclaringClass();//返冋一个用于描述类中定义的构造器、方法或域的Class对象。
    Class[] getExceptionTypes();//(在Constructor和Method类中),返回一个用于描述方法抛出的异常类型的Class对象数组
    int getModifiers();//返回一个用于描述构造器、方法或域的修饰符的整型数值
    String getName();//返冋一个用于描述构造器、方法或域名的字符串
    Class[] getParameterTypes();//(在Constructor和Method类中),返回一个用于描述参数类型的Class对象数组
    Class getReturnType();//(在Method类中),返回一个用于描述返回类型的Class对象
    
    //java.lang.reflect.Modifier 1.1
    static String toString(int modifiers);//返回对应modifiers中位设置的修饰符的字符串表示
    static boolean isAbstract(int modifiers)
    static boolean isFinal(int modifiers);
    static boolean isInterface(int modifiers);
    static boolean isNative(int modifiers);
    static boolean isPrivate(int modifiers);
    static boolean isProtected(int modifiers);
    static boolean isPublic(int modifiers);
    static boolean isStatic(int modifiers);
    static boolean isStrict(int modifiers);
    static boolean isSynchronized(int modifiers);
    static boolean isVolatile(int modifiers);
    //这些方法将检测方法名中对应的修饰符在modifiers值中的位
    

运行时使用反射分析类

  • 获取指定域值(有问题版)

    //1. 初始化一个对象,Employee类有name、salary, month, day, year这5个private域
    Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
    Class cl = harry.getClass();
    /**
    * 2. getField("name"),获取指定域
    * 注意:使用getField()方法需要抛出NoSuchFieldException
    * 这里getField无法获取私有域,会报错
    * 如果改用getDeclaredField,虽然可以获取,但后面使用get获取域值仍然会因访问权限报错
    */
    Field f = cl.getField("name");//获取域”name“,name必须是公有域
    //3. get获取域值,如果正常执行,v将是String对象,值为Harry Hacker;但这里会因访问权限报错
    Object v = f.get(harry);
    
  • 获取指定域值(setAccessible)

    //上述代码修改为
    Field f = cl.getDeclaredField("name");//获取域”name“,name必须是公有域
    f.setAccessible(true);
    Object v = f.get(harry);
    System.out.println(v.toString());
    
  • 示例,打印运行时ArrayList的值

    /*
    * isArray, getComponentType, isPrimitive, setAccessible
    */
    class ObjectAnalyzer{
         private ArrayList<Object> visited = new ArrayList<>();
         public String toString(Object obj){
             if(obj == null) return "null";
             if(visited.contains(obj)) return "...";
             visited.add(obj);
             Class cl = obj.getClass();//注意:即便用Object接的参数,但依然可以找到其实际类信息
             if(cl == String.class) return (String) obj;//判断是不是String类型
             //判断是不是数组类型
             if(cl.isArray()){
                 String r = cl.getComponentType()+"[]{";//ComponentType指组件类型,即去掉一个[]之后的类型
                 /**
                  * 例如:int[][][] aa = new int[5][6][7];
                  * Class cl = aa.getClass();
                  * System.out.println(cl.getComponentType());//输出的是:class [[I
                  * System.out.println(Array.getLength(aa));//输出的是第一维的大小:5
                  */
                 for(int i=0; i<Array.getLength(obj); i++){
                     if(i>0) r+=",";
                     Object val = Array.get(obj, i);
                     if(cl.getComponentType().isPrimitive()) r+=val;//基本类型
                     else r+=toString();//非基本类型
                 }
                 return r+"}";
             }
             //获取obj及其父类的静态域
             String r = cl.getName();//cl是obj的类
             do{
                 r += "[";
                 Field[] fields = cl.getDeclaredFields();
                 /**
                  * 修改整个fields的权限
                  */
                 AccessibleObject.setAccessible(fields, true);
                 for(Field f:fields){
                     if(!Modifier.isStatic(f.getModifiers())){
                         if(!r.endsWith("[")) r+=","; //判断是不是第一个域
                         r += f.getName() + "=";
                         try{
                             Class t = f.getType();
                             /**
                              * Object val = f.get(obj); 获取obj的值
                              */
                             Object val = f.get(obj);
                             if(t.isPrimitive()) r+=val;
                             else r+=toString(val);
                         }catch (Exception e){
                             e.printStackTrace();
                         }
                     }
                 }
                 r += "]";
                 cl = cl.getSuperclass();//如果有超类,还要返回超类的域
             }while(cl != null);
             return r;
         }
      }
    
  • API

    //java.lang.reflect.AccessibleObject 1.2
    void setAccessible(boolean flag);//为反射对象设置可访问标志。flag为true表明屏蔽Java语言的访问检查,使得对象的私有属性也可以被査询和设置
    boolean isAccessible();//返回反射对象的可访问标志的值
    static void setAccessible(AccessibleObject[] array, boolean flag);//是一种设置对象数组可访问标志的快捷方法。
    

使用反射编写泛型数组代码(任意类类型的转换)

需求:将Employee[]转换为Object数据

  • 有问题版本

    public static void main(String[] args) throws Exception{
        Employee[] a = new Employee[5];
        Employee[] newArray = (Employee[]) badCopyOf(a, 10);//报错ClassCastException,Employee无法接收
    }
    public static Object[] badCopyOf(Object[] a, int newLength){
        Object[] newArrary = new Object[newLength];
        System.arraycopy(a, 0, newArrary, 0, Math.min(a.length, newLength));
        return newArrary;
    }
    //编译通过,但运行时报错ClassCastException. 问题:父类不能交给子类引用!
    

    将一个Employee[ ]转换成Object[ ]数组, 然后再把它转换回来是可以的,但一从开始就是Object[]的数组却永远不能转换成Employee[]

    例如可以将上述代码修改为

    public static Object[] badCopyOf(Object[] a, int newLength){
        Employee[] newArrary = new Employee[newLength];
        System.arraycopy(a, 0, newArrary, 0, Math.min(a.length, newLength));
        return newArrary;
    }
    //编译通过,运行通过
    

    但是下面这样原始就是Object[]的数组就永远不能转换为Employee[]了

    public static void main(String[] args) {
    	Object[] objects = new Object[5];
    	Employee[] employees = (Employee[]) objects;
    }
    //编译通过,但运行时报错ClassCastException. 无修改方式
    

为了编写这类通用的数组代码, 需要能够创建与原数组类型相同的新数组

重要代码

扩展任意类型的数组(包括类数组和基本类型数组)

//使用Array的一些方法来操作
public static void main(String[] args) throws Exception{
    Employee[] a = new Employee[5];
    Employee[] newArray = (Employee[]) badCopyOf(a, 10);//报错ClassCastException,Employee无法接收
}
//注意这里的返回值是Object,因为这个Object本身就是一个数组
public static Object badCopyOf(Object[] a, int newLength){
    Class cl = a.getClass();//获得类a的原始类型
    if(!cl.isArray()) return null; //必须是数组类型
    Class componentType = cl.getComponentType(); //获得组件类型,即去掉一维之后的部分
    Object newArray = Array.newInstance(componentType, newLength);//特别注意这里,Array创建的是一个数组
    System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
    return newArray;
}

也可以对基本数据类型进行替换,如

intn a = {1, 2, 3, 4, 5, 6};
a = (int[])goodCopyOf(a, 10);

类似的函数指针

C/C++允许用函数指针执行任意函数,java并不提供这种方式
java认为接口可以interface是一种更好的解决方案,但反射机制却允许了调用任意方法

  • 使用Method对象可以实现函数指针的全部操作
import java.lang.reflect.Method;
public class Test {
    public static void main(String[] args) throws NoSuchMethodException {
        //getMethod,通过方法名和所有参数的class返回方法
        Method sqrt = Math.class.getMethod("sqrt", double.class);
        Method square = Test.class.getMethod("square", double.class);
        printTable(1, 3, 1, sqrt);
        printTable(1, 3, 1, square);
    }
    public static double square(double x){
        return x*x;
    }
    public static void printTable(double from, double to, double step, Method f){
        for(double i=from; i<to; i+=step){
            try{
                /**
                 * 通过反射的机制,可以通过invoke方法来调用类的函数
                 * invoke方法的第一个参数是一个对象。此对象可以为:1.方法持有者;2.方法持有者的继承者。如果该方法是静态方法,可以用null或者用类来代替
                 * 第二个参数是变长的,是调用该方法的参数。
                 */
                double y = (Double)f.invoke(null, i);
                System.out.printf("%10.4f %10.4f\n", i, y);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

反射的应用场景

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法;
  • 在运行时调用任意一个对象的方法;
  • 生成动态代理。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值