Java与C++不同
- 在Java中,所有的继承都是公共继承,而没有C++中的私有继承和 保护继承。
- 在Java中使用关键字super调用超类的方法,而在C++中则采用超类名加::操作符的形式。例如,Manager类的getSalary方法要调用Employee::getSalary而不是super.getSalary().
- 在C++中,如果希望实现动态绑定,需要将成员函数声明为virtual。在Java中,动态绑定是默认的行为。如果不希望让一个方法是虚拟的,可以将它标记为final。
- 在C++中,一个类可以有多个超类。Java不支持多重继承,但可以实现多个接口。
- Java用abstract关键字表示抽象类和方法,C++用纯虚函数表示抽象方法。
- 在Java中,object是所有类的超类;在C++中没有所有类的根类,不过,每个指针都可以转换成void*指针。
- 在Java的老版本中,程序员使用Vector类实现动态数组。不过,ArrayList类更加高效,没有任何理由再使用Vector类。ArrayList类似于C++的vector模板。但是C++的vector模板重载了[]运算符。由于Java没有运算符重载,所以必须调用显式的方法(get、set)。此外,C++向量是按值拷贝。赋值操作a = b将会构造一个与b长度相同的新向量a,并将所有的元素由b拷贝到a,而在Java中,这条赋值语句的操作结果是让a和b引用同一个数组列表。
基本概念
继承:基于已有的类创建新的类。
构造器
-
由于Manager类的构造器不能访问Employee类的私有字段,所以必须通过一个构造器来初
始化这些私有字段。可以利用特殊的super语法调用这个构造器。使用super调用构造器的语
句必须是子类构造器的第一条语句。
ps:一个类的多个构造器之间,也可以互相调用,也必须是第一条语句。 -
如果子类的构造器没有显式地调用超类的构造器,将自动地调用超类的无参数构造器。 如果超类没有无参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造 器,Java编译器就会报告一个错误。
关键字【this】有两个含义:一是指示隐式参数的引用,二是调用该类的其他构造器。
类似地,【super】关键字也有两个含义:一是调用超类的方法,二是调用 超类的构造器。
在调用构造器的时候,this和super这两个关键字紧密相关。调用构造器的语句只能作为另一个构造器的**第一条语句**出现。构
造器参数可以传递给当前类(this)的另一个构造器,也可以传递给超类(super)的构造器。
多态
定义
- 尽管这里将e声明为Employee类型,但实际上e既可以引用Employee类型的对象,也可以引用Manager类型的对象。
- 当e引用Employee对象时,e.getSalary()调用的是Employee类中的getSalary()方法;当e引用Manager对象时,e.getSalary()调用的是Manager类中的getSalary方法。虚拟机知道e实际引用的对象类型,因此能够正确地调用相应的方法。
- 一个对象变量可以指示多种实际类型的现象称为多态,在运行时能够自动地选择适当的方法,称为动态绑定。
- 在C++中,如果希望实现动态绑定,需要将成员函数声明为virtual。在Java中,动态绑定是默认的行为。如果不希望让一个方法是虚拟的,可以将它标记为final。
public class ManagerTest
{
public static void main(String[] args)
{
// construct a Manager object
var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
var staff = new Employee[3];
// fill the staff array with Manager and Employee objects
staff[0] = boss; // staff[0]是employee类型,但指向了boss类型,而且可以调用boss的属性和方法
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
}
}
动态绑定有一个非常重要的特性:无须对现有的代码进行修改就可以对程序进行扩展。 假设增加一个新子类Executive,如果e恰好引用一个Executive类的对象,就会自动地调用 Executive.getSalary()方法。
超类变量可以引用所有的子类对象,但子类变量不能引用超类对象。
因为manager实际是一个Employee对象,无法调用Manager特有的方法。
为什么子类变量不能引用超类对象?
- 当超类变量Employee引用子类对象Manager时,Employee只能调用Employee的方法,而这些方法的实现Manager一定有,只是可能Manager重写了Employee的方法。会带来多态和动态绑定。
- 如果用子类变量指向超类对象时,子类变量manager会调用manager特有的方法如getBonus、setBonus,但超类对象实际并没有这两个方法。这是极大的问题。
子类引用的数组可以转换成超类引用的数组
在Java中,子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换。因为子类Manager一定是一个超类Employee。注意此时:staff和managers引用的是同一个数组。
但会有奇怪的问题,staff[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1); // 编译器竟然接纳了这个赋值操作
。当调用 managers[0].setBonus(1000)
的时候,将会试图调用一个不存在的实例字段,进而搅乱相邻存储空间的内容。为了确保不发生这类破坏,所有数组都要牢记创建时的元素类型,并负责监督仅将类型兼容的引用存储到数组中。
Employee[] staff = new Employee[3];
Manager[] managers=new Manager[3]; //子类引用的数组
staff=managers; // 子类引用的数组被转换成超类引用的数组
staff[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1); // 编译器竟然接纳了这个赋值操作
覆写返回子类型
方法的名字和参数列表称为方法的签名。例如,f(int)和f(String)是两个有相同名字、不同签名的方法。
返回类型不是签名的一部分。两个方法不能名字和参数相同,但返回类型不同。但允许子类将覆盖方法的返回类型改为原返回类型的子类型。
public Employee getBuddy() { . . . }
public Manager getBuddy() { ... } // OK to change return type
强制类型转换
- 只能在继承层次内进行强制类型转换。
- 在将超类强制转换成子类之前,应该使用instanceof进行检查。
ps:超类变量引用子类对象,由于多态和自动绑定,不需要强制类型转换,直接可用。
staff[0] = boss;
只有当,用子类变量引用超类对象时,需要强制类型转换。但有个前提:这个超类对象实际引用的必须是对应的子类对象,否则会抛出ClassCastException异常。所以应该先用instanceof运算符进行检查。
instanceof是Java中的二元运算符,左边是对象,右边是类;当对象是右边类或子类所创建对象时,返回true;否则,返回false。如果左边对象为null,返回false,不抛异常。
public static void main(String[] args)
{
// construct a Manager object
var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
var staff = new Employee[3];
// fill the staff array with Manager and Employee objects
staff[0] = boss; // staff[0]是employee类型,但指向了boss类型,而且可以调用boss的属性和方法
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
if(staff[0] instanceof Manager) // instanceof运算符
{
System.out.println("staff[0]可强制转换");
Manager manager=(Manager)staff[0]; //可行
}
else {
System.out.println("staff[0]不可强制转换");
}
if(staff[1] instanceof Manager)
{
System.out.println("可强制转换");
Manager manager1=(Manager)staff[1];
}
else {
System.out.println("staff[1]不可强制转换");
}
// print out information about all Employee objects
}
阻止继承:final类和方法
- final类:阻止派生Executive类的子类,就可以在声明这个类的时候使用final修饰符。声明格式如下所示:
如String类是final类,这意味着不允许任何人定义String的子类。
public final class Executive extends Manager { ... }
- final方法:子类就不能覆盖这个方法。final类中所有方法自动变成final方法。
public class Employee
{
public final String getName()
{
return name;
}
}
final字段表示构造对象之后就不允许改变它们的值了。final类中所有方法自动变成final方法,final类中的字段不会自动变成final字段。
多态 vs final ?
多态的动态绑定会带来额外开销,特别是不被覆写的简单方法可以被内联进一步优化性能,所以以前final确实可以提高效率。
但是虚拟机中的即时编译器比传统编译器的处理能力强得多,可以准确地知道类之间的继承关系,并能够检测出是否有类确实覆盖了给定的方法,基本消除了性能问题。
abstract抽象方法
- 抽象方法不需要实现。留给子类实现。
public abstract String getDescription();
- 包含一个或多个抽象方法的类本身必须被声明为抽象的。
- 除了抽象方法之外,抽象类还可以包含字段和具体方法。\
- 即使不含抽象方法,也可以将类声明为抽象类。
- 抽象类不能实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象Person p =
new Person(“Vince Vu”); // error 。可以定义一个抽象类的对象变量,但是这样一个变量只能引用非抽象子类的对象。Person p = new Student("Vince Vu", "Economics");
public abstract class Person {
private String name;
public Person(String name) {
this.name = name;
}
public abstract String getDescription();
public String getName() {
return name;
}
}
在C++中,有一种抽象方法称为纯虚函数,要在末尾用=0标记.如果至少有一个纯虚函数,这个C++类就是抽象类。在C++中,没有提供表示抽象类的特殊关键字。例如:
class Person // C++
{
public:
virtual string getDescription() = 0;
};
访问控制修饰符
修饰符 | 控制 |
---|---|
private | 仅对本类可见 |
public | 对外部完全可见 |
public | 对外部完全可见 |
protected | 对本包所有类和所有子类可见 |
默认 | 对本包可见 |
object所有类的超类
在Java中,只有基本类型不是对象,例如,数值、字符和布尔类型的值都不是对象。
所有的数组类型,不管是对象数组还是基本类型的数组都扩展了 Object类。
Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK
在C++中没有所有类的根类,不过,每个指针都可以转换成void*指针。
优先使用对空安全的Objects.equals()、Objects.hashCode()方法。
为了防备可能为null的情况,需要使用Objects. equals方法。如果两个参数都为null, Objects.equals(a, b)调用将返回true ;如果其中一个参数为null,则返回false ;否则,如果两个参数都不为null,则调用a.equals(b)。
euqals()方法
举例覆写Employee的equals方法
Object类中实现的equals方法将确定两个对象引用是否相等。
判断两个对象内容相等,需要自己覆写equals方法。如下:
为了防备name或hireDay可能为null的情况,需要使用Objects. equals方法。如果两个参数都为null, Objects.equals(a, b)调用将返回true ;如果其中一个参数为null,则返回false ;否则,如果两个参数都不为null,则调用a.equals(b)。
Employee.equals方法的最后一条语句要改写为:
return Objects.equals(name, other.name)
&& salary == other.salary
&& Objects.equals(hireDay, other.hireDay);
子类equals方法
在子类中定义方法时,首先调用超类的equals()方法。如果检测失败,对象就不可能相等。如果超类中的字段都相等,就需要比较子类中的实例字段。
public class Manager extends Employee
{
public boolean equals(Object otherObject)
{
if (!super.equals(otherObject)) return false;
// super.equals checked that this and otherObject belong to the same class
Manager other = (Manager) otherObject;
return bonus = other.bonus;
}
}
编写完美的equals方法:
多态参数类型不同的错误
hashcode()方法
equals与hashCode的定义必须相容
equals与hashCode的定义必须相容:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()返回相同的值。例如,如果定义Employee.equals比较员工的ID,那么hashCode方法就需要散列ID,而不是员工的姓名或存储地址。
objects.hash()
toString()方法
打印数组,调用Arrays.toString()。打印多维数组,调用Arrays.deepToString()。
Object类定义了 toString方法,可以打印对象的类名和散列码。例如,调用
System.out.println(System.out)
// 将生成以下输出:
// java.io.PrintStream@2f6684
对象包装器与自动装箱
- 对象包装器:基本类型都有一个与之对应的类。
包装器类是不可变的(同String),即一旦构造了包装器,就不允许更改包装在其中的值。同时,包装器类还是final,因此不能派生它们的子类。
- 尖括号中的类型参数不允许是基本类型, 要用包装器。
var list = new ArrayList <int>();
var list = new ArrayList<Integer>();
-
自动装箱(如:int → Integer);自动拆箱(如:Integer → Integer)。
-
包装器和基本类型不同:以下代码检测a和b的地址是否相同。因为a和b是对象。应该用equals方法。
Integer a = 1000;
Integer b = 1000;
if (a == b)...
- 包装器类可以引用null,可能NullPointerException。
Integer n = null;
System.out.printin(2 * n); // throws NullPointerException
- 在一个条件表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double:
Integer n = 1;
Double x = 2.0;
System.out.printIn(true ? n : x); // prints 1.0
Integer的方法:static String toString(int i)、static int parseInt(String s)
参数数量可变的方法
这里的省略号…是Java代码的一部分,它表明这个方法可以接收任意数量的对象(除fmt参数之外)。
public class PrintStream
{
public Printstream printf(String fmt, Object... args) { return formatffmt, args); }
}
枚举类
- 枚举类可以简单定义及使用。SMALL, MEDIUM, LARGE, EXTRALARGE表示Size类的四个对象实例。
public class EnumTest {
public static void main(String[] args) {
Size small = Size.SMALL;
System.out.println(small);
}
}
enum Size{
SMALL, MEDIUM, LARGE, EXTRALARGE
}
-
所有枚举类都继承了Enum类。
-
枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用。
-
枚举类一旦被使用,就会用private构造函数构造所所有的实例。如图call了SMALL, MEDIUM, LARGE, EXTRALARGE。在SMALL(“S”)中,SMALL是枚举常量名,S表示枚举类的字段abbreviation。
import java.util.*;
/**
* This program demonstrates enumerated types.
*
* @version 1.0 2004-05-24
* @author Cay Horstmann
*/
public class EnumTest {
public static void main(String[] args) {
var in = new Scanner(System.in);
System.out.print("Enter a size: (SMALL, MEDIUM, LARGE, EXTRA_LARGE) ");
String input = in.next().toUpperCase();
Size size = Enum.valueOf(Size.class, input);
System.out.println("size=" + size);
System.out.println("abbreviation=" + size.getAbbreviation());
if (size == Size.EXTRA_LARGE)
System.out.println("Good job--you paid attention to the _.");
}
}
enum Size {
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private Size(String abbreviation) {
this.abbreviation = abbreviation;
System.out.println("Size() is called for " + this);
}
public String getAbbreviation() {
return abbreviation;
}
private String abbreviation;
public String toString() {
return super.toString() + "(\"" + this.abbreviation + "\")";
}
}
- 枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它。
反射
反射:能够分析类能力的程序称为反射。
反射机制可以用来:
- 在运行时分析类的能力。
- 在运行时检查对象,例如,编写一个适用于所有类的toString方法。
- 实现泛型数组操作代码。
- 利用Method对象,调用任意方法和构造器,这个对象很像C++中的函数指针。
Class类
获得class类对象的方法:
- Object类中的getClass()方法将会返回一个Class类型的实例。getNanie()方法将返回类的名字。
Employee e;
Class cl = e.getClass();
- 静态方法forName获得类名对应的Class对象
String className = "java.util.Random";
Class cl = Class.forName(className);
- 用.class产生匹配的类对象。
Class cl1 = Random.class; // if you import java.util.*;
Class cl2 = int.class;
Class cl3 = Double[].class;
请注意,一个Class对象实际上表示的是一个类型,这可能是类,也可能不是类。例如,int不是类,但int.class是一个Class类型的对象。
==比较两个类对象
if (e.getClass() == Employee.class)...
如果e是一个Employee实例,这个测试将通过。与条件e instanceof Employee不同,如果e
是某个子类(如Manager)的实例,这个测试将失败。
用class对象构造类实例。
var className = "java.util.Random"; // or any other name of a class with
// a no-arg constructor
cl = Class.forName(className);
Object obj = cl.getConstructor().newInstance();
声明异常入门
如果一个方法包含一条可能抛出检查型异常的语句,则在方法名上增加一个throws子句。
public static void doSomethingWithClass(String name)
throws ReflectiveOperationException
{
Class cl = Class.forName(name); // might throw exception
System.out.println(cl.getName());
//do something with cl
}
调用这个方法的任何方法也都需要一个throws声明。这也包括main方法。如果一个异常确实出现,main方法将终止并提供一个堆栈轨迹。
资源
package corejava.v1ch05.resources;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import javax.swing.*;
/**
* @version 1.5 2018-03-15
* @author Cay Horstmann
*/
public class ResourceTest
{
public static void main(String[] args) throws IOException
{
Class cl = ResourceTest.class;
URL aboutURL = cl.getResource("about.gif");
var icon = new ImageIcon(aboutURL);
System.out.println(System.getProperty("user.dir"));
InputStream stream = cl.getResourceAsStream("data/about.txt"); //相对路径
var about = new String(stream.readAllBytes(), "UTF-8");
InputStream stream2 = cl.getResourceAsStream("/corejava/v1ch05/resources/data/title.txt"); //绝对路径前缀和包名相同
var title = new String(stream2.readAllBytes(), StandardCharsets.UTF_8).trim();
JOptionPane.showMessageDialog(null, about, title, JOptionPane.INFORMATION_MESSAGE, icon);
}
}
利用反射分析类的能力
这个程序将提醒用户输入类名,然后输出类中所有的方法和构造器的签名,以及全部实例字段名。
package corejava.v1ch05.reflection;
import java.util.*;
import java.util.ArrayList;
import java.lang.reflect.*;
/**
* This program uses reflection to print all features of a class.
* @version 1.11 2018-03-16
* @author Cay Horstmann
*/
public class ReflectionTest
{
public static void main(String[] args)
throws ReflectiveOperationException
{
// read class name from command line args or user input
String name;
if (args.length > 0) name = args[0];
else
{
var in = new Scanner(System.in);
System.out.println("Enter class name (e.g. java.util.Date): ");
name = in.next();
}
// print class name and superclass name (if != Object)
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print("class " + name);
if (supercl != null && supercl != Object.class) System.out.print(" extends "
+ supercl.getName());
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
}
/**
* Prints all constructors of a class
* @param cl a class
*/
public static void printConstructors(Class cl)
{
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors)
{
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(name + "(");
// print parameter types
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(");");
}
}
/**
* Prints all methods of a class
* @param cl a class
*/
public static void printMethods(Class cl)
{
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods)
{
Class retType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
// print modifiers, return type and method name
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(retType.getName() + " " + name + "(");
// print parameter types
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(");");
}
}
/**
* Prints all fields of a class
* @param cl a class
*/
public static void printFields(Class cl)
{
Field[] fields = cl.getDeclaredFields();
for (Field f : fields)
{
Class type = f.getType();
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 + ";");
}
}
}
Enter class name (e.g. java.util.Date):
java.util.ArrayList
public class java.util.ArrayList extends java.util.AbstractList
{
public java.util.ArrayList(java.util.Collection);
public java.util.ArrayList();
public java.util.ArrayList(int);
public boolean add(java.lang.Object);
private void add(java.lang.Object, [Ljava.lang.Object;, int);
public void add(int, java.lang.Object);
public boolean remove(java.lang.Object);
public java.lang.Object remove(int);
public java.lang.Object get(int);
public boolean equals(java.lang.Object);
public int hashCode();
public java.lang.Object clone();
public int indexOf(java.lang.Object);
public void clear();
public int lastIndexOf(java.lang.Object);
public boolean isEmpty();
public void replaceAll(java.util.function.UnaryOperator);
public int size();
public java.util.List subList(int, int);
public [Ljava.lang.Object; toArray([Ljava.lang.Object;);
public [Ljava.lang.Object; toArray();
public java.util.Iterator iterator();
public boolean contains(java.lang.Object);
public java.util.Spliterator spliterator();
public boolean addAll(java.util.Collection);
public boolean addAll(int, java.util.Collection);
public java.lang.Object set(int, java.lang.Object);
private void readObject(java.io.ObjectInputStream);
private void writeObject(java.io.ObjectOutputStream);
public void forEach(java.util.function.Consumer);
public void ensureCapacity(int);
public void trimToSize();
java.lang.Object elementData(int);
private [Ljava.lang.Object; grow(int);
private [Ljava.lang.Object; grow();
int indexOfRange(java.lang.Object, int, int);
int lastIndexOfRange(java.lang.Object, int, int);
private void rangeCheckForAdd(int);
private void fastRemove([Ljava.lang.Object;, int);
private boolean equalsArrayList(java.util.ArrayList);
boolean equalsRange(java.util.List, int, int);
private void checkForComodification(int);
int hashCodeRange(int, int);
private static java.lang.String outOfBoundsMsg(int, int);
private java.lang.String outOfBoundsMsg(int);
private void shiftTailOverGap([Ljava.lang.Object;, int, int);
boolean batchRemove(java.util.Collection, boolean, int, int);
static java.lang.Object elementAt([Ljava.lang.Object;, int);
public boolean removeIf(java.util.function.Predicate);
boolean removeIf(java.util.function.Predicate, int, int);
private static [J nBits(int);
private static void setBit([J, int);
private static boolean isClear([J, int);
private void replaceAllRange(java.util.function.UnaryOperator, int, int);
public void sort(java.util.Comparator);
protected void removeRange(int, int);
public boolean removeAll(java.util.Collection);
public boolean retainAll(java.util.Collection);
public java.util.ListIterator listIterator();
public java.util.ListIterator listIterator(int);
void checkInvariants();
private static final long serialVersionUID;
private static final int DEFAULT_CAPACITY;
private static final [Ljava.lang.Object; EMPTY_ELEMENTDATA;
private static final [Ljava.lang.Object; DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
transient [Ljava.lang.Object; elementData;
private int size;
}
使用反射在运行时分析对象
利用反射机制可以查看在编译时还不知道的对象字段。
public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException
{
var harry = new Employee("Harry Hacker", 50000, 1989, 1, 19);
Class cl = harry.getClass();
// the class object representing Employee
Field f = cl.getDeclaredField("name");
// the name field of the Employee class
f.setAccessible(true); // now OK to call f.get(harry)
Object v = f.get(harry);
System.out.println(v);
// the value of the name field of the harry object, i.e.,
// the String object "Harry Hacker"
f.set(harry, "harry Junior");
System.out.println(harry.getName());
}
一个可用于任意类的通用toString方法(见程序清单5-15)。这个通用toString方法使用getDeclaredFileds获得所有的数据字段,然后使setAccessible便利方法将所有的字段设置为可访问的。对于每个字段,将获得名字和值。通过递归调用toString方法,将每个值转换成字符串。可用于
利用反射编写泛型数组
最关键的是Array类的静态方newlnstance,这个方法能够构造一个新数组。在调用这个方法时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。
Object newArray=Array.newlnstance(componentType,newLength);
为了能够具体实现,需要获得新数组的长度和元素类型。
可以通过调用Array.getLength(a)获得数组的长度。Array类的静态getLength方法会返回一
个数组的长度。要获得新数组的元素类型,就需要完成以下工作:
- 首先获得a数组的类对象。
- 确认它确实是一个数组。
- 使用Class类的getComponentType方法(只为表示数组的类对象定义了这个方法)确定数组的正确类型。
public static Object goodCopyOf(Object a, int newLength)
{
Class cl = a.getClass(); // 虽然a是object类型,但实际指向子类型即一个数组
if (!cl.isArray()) return null;
Class componentType = cl.getComponentType();
int length = Array.getLength(a); //不可能调用a.getLength,因为此时a为object类型
Object newArray = Array.newInstance(componentType, newLength); // 虽然a是object类型,但实际指向子类型即一个数组
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength)); // System.arraycopy的前两个参数都是object类型
return newArray;
}
Object[] ArrayList.toArray()源代码里创建的就是Object[],不能强转成T类型。
调用任意方法和构造器
在C和C++中,可以通过一个函数指针执行任意函数。从表面上看,Java没有提供方法指针,也就是说,Java没有提供途径将一个方法的存储地址传给另外一个方法,以便第二个方法以后调用。事实上,Java的设计者曾说过:方法指针是很危险的,而且很容易出错。他们认为Java的接口 (interf ace)和lambda表达式(将在下一章讨论)是一种更好的解决方案。不过,反射机制允许你调用任意的方法,Method类有一个invoke方法。
invoke的参数和返回值必须是Object类型。这就意味着必须来回进行多次强制类型转换。这样一来,编译器会丧失检査代码的机会,以至于等到测试阶段才会发现错误,而这个时候查找和修正错误会麻烦得多。不仅如此,使用反射获得方法指针的代码要比直接调 用方法的代码慢得多。
有鉴于此,建议仅在绝对必要的时候才在你自己的程序中使用Method对象。通常更好的做法是使用接口以及Java 8引人的lambda表达式(第6章中介绍)。特别要强调:我们建议Java开发者不要使用回调函数的Method对象。可以使用回调的接口,这样不仅代码的执行速度更快,也更易于维护。
import java.lang.reflect.*;
/**
* This program shows how to invoke methods through reflection.
* @version 1.2 2012-05-04
* @author Cay Horstmann
*/
public class MethodTableTest
{
public static void main(String[] args)
throws ReflectiveOperationException
{
// get method pointers to the square and sqrt methods
Method square = MethodTableTest.class.getMethod("square", double.class);
Method sqrt = Math.class.getMethod("sqrt", double.class);
// print tables of x- and y-values
printTable(1, 10, 10, square);
printTable(1, 10, 10, sqrt);
}
/**
* Returns the square of a number
* @param x a number
* @return x squared
*/
public static double square(double x)
{
return x * x;
}
/**
* Prints a table with x- and y-values for a method
* @param from the lower bound for the x-values
* @param to the upper bound for the x-values
* @param n the number of rows in the table
* @param f a method with a double parameter and double return value
*/
public static void printTable(double from, double to, int n, Method f)
throws ReflectiveOperationException
{
// print out the method as table header
System.out.println(f);
double dx = (to - from) / (n - 1);
for (double x = from; x <= to; x += dx)
{
double y = (Double) f.invoke(null, x);
System.out.printf("%10.4f | %10.4f%n", x, y);
}
}
}
继承的设计技巧
问题:
Protected的访问权限
在Java中,保护字段只能由同一个包中的类访问。现在考虑一个Administrator子类,这个子类在另一个不同的包中。Administrator类中的方法只能查看Administrator对象自己的hireDay字段,而不能查看其他Employee对象的这个字段。有了这个限制,就能避免滥用保护机制,不能通过派生子类来访问受保护的字段。
package test1中的类Neighbor,可以访问同一个包中,Person的getMoney方法。
package test1;
public class Person {
private String name;
private int money;
public Person(String name,int money) {
// TODO Auto-generated constructor stub
this.name=name;
this.money=money;
}
public String getName() {
return name;
}
protected int getMoney() {
return money;
}
}
package test1;
public class Neighbor {
public Neighbor() {
// TODO Auto-generated constructor stub
}
public static void main(String[] agrs)
{
Person person = new Person("A", 1000);
System.out.println(person.getMoney());
}
}
package test2中的类Son,可以访问不同包中,Person类的getMoney方法。但不能通过Person对象访问getMoney方法。
package test2;
import test1.Person;
public class Son extends Person {
public Son(String name, int money) {
// TODO Auto-generated constructor stub
super(name, money);
}
public static void main(String[] args) {
Son son = new Son("B", 20);
System.out.println(son.getMoney()); //work
/Person person = new Person("A", 1000);
System.out.println(person.getMoney()); //error
Person sperson = new Son("A", 1000);
System.out.println(sperson.getMoney()); //error
}
}
理解Son自己的getMoney方法
- 在package test2中,新建一个类Nobody。Nobody和Son在同一个包中,但不能访问getMoney方法。因为此时访问的是父类Person的getMoney方法,不能在test1包外访问,子类也只能访问自己的getMoney方法。
package test2;
import test1.Person;
public class Nobody {
public Nobody() {
// TODO Auto-generated constructor stub
}
public static void main(String[] args) {
Son son = new Son("B", 20);
System.out.println(son.getMoney()); //error,这里访问的是父类Person的getMoney方法,不能在这里访问
Person person = new Person("A", 1000);
System.out.println(person.getMoney()); //error,这里访问的是父类Person的getMoney方法,不能在这里访问
Son sperson = new Person("A", 1000);
System.out.println(sperson.getMoney()); //error,这里访问的是父类Person的getMoney方法,不能在这里访问
}
}
- 如果给Son重写getMoney方法,在Nobody类中,就可以访问Son自己的getMoney方法。
package test2;
import test1.Person;
public class Son extends Person {
public Son(String name, int money) {
// TODO Auto-generated constructor stub
super(name, money);
}
protected int getMoney() { //重写
return super.getMoney();
}
}
package test2;
import test1.Person;
public class Nobody {
public Nobody() {
// TODO Auto-generated constructor stub
}
public static void main(String[] args) {
Son son = new Son("B", 20);
System.out.println(son.getMoney()); //work,这里访问的是Son自己的getMoney方法,可被同一个包中的Nobody访问
Person person = new Person("A", 1000);
System.out.println(person.getMoney()); //error,这里访问的是父类Person的getMoney方法,不能在这里访问
Son sperson = new Person("A", 1000);
System.out.println(sperson.getMoney()); //error,这里访问的是父类Person的getMoney方法,不能在这里访问
}
}
private为什么能保护数据安全?即封装的优点
因为只能通过指定的方法如get、set访问,而set里会有相应的处理逻辑,防止任意改变private值,出现错误。例如set(pos, ‘a’)方法,会检查pos是否合法,如果直接操作数组,可能越界等错误。
如果将name、salary和hi reDay字段标记为公共,而不是编写单独的访问器方法,难道不是更容易一些吗?
不过,name是一个只读字段。一旦在构造器中设置,就没有任何办法可以对它进行修改, 这样我们可以确保name字段不会受到外界的破坏。
虽然salar y不是只读字段,但是它只能用rai seSalar y方法修改。特别是一旦这个值出现了错误,只需要调试这个方法就可以了。如果salary字段是公共的,破坏这个字段值的捣乱者有可能会出没在任何地方(那就很难调试了)。
封装理解
public、private、protected、default不是谁能访问,能不能访问的意思,而是在哪里
可以写访问代码。
如果是private,访问的代码一定写在类里。如果是public,所有import了的位置,都可以写访问代码。而pulic的方法可以让private的属性在任意位置以固定的方式被访问和修改。