MENU

Java笔记(二):继承

May 23, 2019 • Read: 103 • 技术笔记阅读设置

摘自《Java 核心技术 卷一》第五章

第五章 继承

“is-a”关系是继承的一个明显特征,例如每个经理都是一名雇员。

继承用关键字extends表示,子类一般比超类拥有更多的功能。

子类中的覆盖方法不能直接访问超类的私有域!所以需要超类提供一些接口比如getSalary()方法(访问器)

运行超类的方法用关键字super

@Override
public double getSalary() {
    // 调用超类提供的访问器接口
    return super.getSalary() + this.bonus;
}

public Manager(String name, double salary, int year, int month, int day) {
    // 调用超类的构造方法
    super(name, salary, year, month, day);
    bonus = 0;
}

关键字this的两个用途:

  1. 引用隐式参数 如this.xxx
  2. 调用该类的其他构造器 如this(....)

关键字super也有两个用途:

  1. 调用超类的方法
  2. 调用超类的构造器

在调用构造器的时候,两个关键字的使用方法很相似,都是作为另一个构造器的第一条语句出现。

Java本身不支持多继承

Java中的多态

在Java中对象变量是多态的,一个超类变量可以引用超类对象,也可以引用该超类的任何一个子类对象。

反之,不能将一个超类的引用赋给子类变量。

如果子类中定义了一个与超类签名相同的方法,那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。在覆盖一个方法时,子类方法不能低于超类方法的可见性,特别是如果超类方法是public,子类一定要声明为public。

如果是private方法、static方法、final方法或者构造器,那么编译器可以准确地知道应该调用哪个方法,称为静态绑定

阻止继承:final类和方法

不允许被扩展的类被称为final类,不允许被覆盖的方法称为final方法

final类中的所有方法都会自动称为final方法,但是不包括域!

对象引用的强制类型转换

  • 只有在继承层次内进行类型转换
  • 在将超类转换成子类之前,应该使用instanceof进行检查
if (staff[1] instanceof Manager) {
    boss = (Manager) staff[1];
    ...
}

应该尽量少类型转换和instanceof运算符。

抽象类

public abstract String getDescription();  // 抽象方法不需要定义内容

包含一个或多个抽象方法的类本身必须被声明为抽象的。但类即使不含抽象方法,也可以声明为抽象类。

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

但是抽象类除了包含抽象方法外,依然可以包含具体数据和方法

抽象类不能被实例化,但是可以创建具体子类的对象;抽象类的对象变量只能引用非抽象子类的对象。

Person p = new Student("name", "descriptions");  // ok
Person p = new Person("name");  // error!

实际上在此处,由于不能构造抽象类的对象,变量p永远不会引用Person对象,而是引用如Student这样具体的子类对象

受保护的访问:protected

private:子类也不能访问超类的私有域

如果希望超类中某些方法允许被子类访问,或允许子类访问超类中的某个域,可以将这些方法声明为protected

实际上,Java中的受保护部分对所有子类及同一个包中所有其他类都可见….

归纳用于控制可见性的访问修饰符:

  1. 仅对本类可见:private
  2. 对所有类可见:public
  3. 对本包和子类可见protected
  4. 对本包可见:默认,不需要修饰符

Object:所有类的超类

Object类是所有类的始祖,如果没有明确指出超类,Object就被认为是这个类的超类。

Object obj = new Employee("Harry Hacker", 45000);  // 可以用Object类型的变量引用任何类型的对象

在Java中,只有基本类型不是对象,所有数组类型,不管是对象数组or基本类型数组都继承自Object类

Employee[] employees = new Employee[10];
Object obj = employees;  // 可以引用
obj = new int[10];  // 也可以引用

equals方法

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;

    if (obj == null) return false;

    // 比较两个对象是否属于一个类,getClass()会返回一个对象所属的类
    if (getClass() != obj.getClass()) return false;

    // 先强制转换然后比较实例域
    Employee other = (Employee) obj;

    return Objects.equals(name, other.name)
            && salary == other.salary
            && Objects.equals(hireDay, other.hireDay);
}

为了防备name或hireDay可能为null,需要使用Objects.equals(a, b)方法

如果两个参数都为null 则返回true;

如果一个为null,则返回false;

如果两个都不为null,则调用a.equals(b)

子类中定义equals,首先调用超类的equals方法,如果超类中的域都相等,再比较子类中的域。

class Manager extends Employee {
    ...
    @Override
    public boolean equals(Object obj) {
        // 先检测超类的equals
        if (!super.equals(obj)) return false;

        Manager manager = (Manager) obj;
        return bonus == manager.bonus;
    }
}

编写完美equals方法的建议:

编写完美equals方法的建议

hashCode方法

散列码是由对象导出的一个整形值。如何x和y是两个不同的对象,则x.hashCodey.hashCode基本不会相同。

hashCode方法来自Object类,所以每个对象都有一个默认的散列码。

如果重新定义equals方法,就必须重新定义hasCode方法,以便用户可以将对象插入到散列表中。

hasCode方法应该返回一个整形数值(可以为负数)

public int hashCode() {
    return 7 * Objects.hashCode(name)
        + 11 * Double.hashCode(salart)
        + 13 * Objects.hashCode(hireDay)
}

// 更好的做法
// 组合多个散列值
public int hashCode() {
    return Objects.hash(name, salary, hireDay);
}

Equals和hashCode的定义必须一致,如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。比如用Employee.equals比较雇员的ID,那么hashCode就必须散列ID,而不是其他字段(姓名、地址)。

数组元素散列用Arrays.hashCode

toString方法

只要对象与一个字符串通过操作符“+”连接起来,Java编译就会自动调用toString方法。

Object类定义了toString方法,用来打印输出对象所属的类名和散列码

System.out.println(System.out);
// 输出java.io.PrintStream@61bbe9ba

这是因为PrintStream没有override这个toString方法。

数组继承了Object类的toString方法,会产生如下结果:

int[] a = new int[10];
out.println(a);
// [I@61bbe9ba

[I表示一个整形数组,修正的方法是使用Arrays.toString

String s = Arrays.toString(a);
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

要打印多维数组,需要使用Arrays.deepToString方法

强烈建议自定义的每一个类增加toString方法,用这个类的程序员会在日志记录中获益


泛型数组列表

为了解决动态调整数组大小的问题,Java提供了ArrayList类,它是一个采用类型参数的泛型类

ArrayList<Employee> staff = new ArrayList<>();  // Java SE 7支持

如果已经清楚或能够估计出数组可能存储的元素数量,可以在填充数组之前调用ensureCapacity方法。

staff.ensureCapacity(100);

这个方法调用将分配一个包含100个对象的内部数组,然后调用100次add,而不用重新分配空间。否则当内部数组满了,ArrayList会自动创建一个更大的数组,并将其中的所有对象从小的数组拷贝到较大的数据。

还可以将初始容量传递给构造器

ArrayList<Employee> staff = new ArrayList<>(100);
staff.size();  // 返回数组列表中实际元素的数目

注意:

new ArrayList<>(100)  // capacity is 100
new Employee[100]  // size is 100

这两种表示方法是不同的,如果为数组分配100个元素的存储空间,则数组就有100个空位置可以用

而容量为100个元素的数组列表只是拥有保存100个元素的潜力,但是在最初,甚至在完成初始化构造之后,数组列表根本就不含有任何元素。

一旦能够确认数组列表的大小不再发生变化,就使用trimToSize()方法,它将存储区域的大小调整为当前元素数量所需要的存储空间数目,垃圾回收器将回收多余的存储空间。

staff.set(i, harry);  // 设置第i个元素 只是替换数组中已经存在的元素内容
staff.get(i);  // 获得数组列表的元素

对象包装器与自动装箱

Integer类对应基本类型int,这些类称为包装器
(Integer、Long、Float、Double、Short、Byte、Character、Void、Boolean)

对象包装器类是不可变的,一旦构造了包装器,就不允许更改包装在其中的值。

对象包装器类是final,因此不能定义他们的子类。

ArrayList<Integer> list = new ArrayList<>();

ArrayList<Integer>的效率远低于int[]

当执行:

list.add(3);

将自动变换成:

list.add(Integer.vauleOf(3));

这种变换称为自动装箱

相反,将Integer赋给int时,将会自动拆箱:

int a = list.get(i);  
// 将会翻译成
int a = list.get(i).intValue();

基本类型和其对象包装器判断相等问题

int a = 1000;
Integer b = 1000;
if (a == b) // true
  
Integer a = 1000;
Integer b = 1000;
if (a == b) // false

从上面的第二个例子可以看到,"=="运算符检测的是对象是否在同一存储区域,因而二者不相等,但是在Java中却有可能让他们相等!

Integer a = 100;
Integer b = 100;
if (a == b) // true

此处相等的原因:

自动装箱的规范要求boolean、byte、char<=127。介于-128~127之间的short和int被包装到固定的对象中,故相等。

这个结果不是我们所期望的,所以两个包装器比较时候尽量用equals

如果一个表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double

Integer a = 3;
Double b = 1.0;
out.println(true ? a : b);  // 3.0

int c = 3;
double d = 1.0;
out.println(true ? c : d);  // 也是3.0

可以将某些基本方法放置在包装器中,如:

int x = Integer.parseInt(s);

这里与Integer对象没有任何关系,parseInt是一个静态方法。

参数数量可变的方法

Java中的printf方法是这样定义的:

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

此处的Object ...args代表这个方法可以接收任意数量的对象

此处的Object…argsObject[] args完全一样,如下调用等价:

System.out.printf("%d %s", new Integer(1), "weight");
System.out.printf("%d %s", new Object[] {new Integer(1), "weight"});

可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,如:

public static void main(String ...args) {...}

枚举类

public static void main(String args[]) {
  Scanner sc = new Scanner(System.in);
  System.out.println("Enter a size(SMALL, MEDIUM, LARGE, EXTRA_LARGE)");
  String input = sc.next().toUpperCase();
  // valueOf:返回指定名字、给定类的枚举常量
  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!");
  }
  // 返回枚举常量在enum声明中的位置,从0开始计数
  System.out.println(Size.LARGE.ordinal()); // 2
}

enum Size {
  SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

  private String abbreviation;

  private Size(String abbreviation) {
    this.abbreviation = abbreviation;
  }

  public String getAbbreviation() {
    return this.abbreviation;
  }
}

反射

Class类

Java运行时所有的对象维护一个被称为"运行时"的类型标识,这个信息跟踪着每个对象所属的类。

可以用专门的Java类访问这些信息,这个类称为Class类,Object类的getClass()方法会返回一个Class类型的实例。

Class cl = e.getClass();

一个Class对象表示一个特定类的属性,最常用的方法是getName,返回类的名字。

还可以调用静态方法forName获得类名对应的Class对象

String className = "java.util.Random";
Class cl = Class.forName(className);

解决大型应用程序启动时加载过多类的方法

main方法的类没有显式地引用其他的类,首先显示一个启动画面,然后通过Class.forName手工地加载其他的类。

获得Class类对象的第三种方法很简单——T.class,T是任意Java类型

Class cl1 = Random.class;  // if u import java.util.*
Class cl2 = int.class;  // int
Class cl3 = Double[].class;  // [Ljava.lang.Double

Class类实际上是一个泛型类

getName方法在应用于数组类型的时候会返回很奇怪的名字,如上面例子的Double[].class

可以利用==运算符实现两个Class对象的比较

if (e.getClass() == Employee.class) ...

还可以用Class类中的方法newInstance()动态创建一个类的实例:

Employee el = e.getClass().newInstance();
// newInstance会调用默认构造器初始化新建的对象,如果这个类没有默认构造器,则抛出异常

forNamenewInstance配合使用,可以根据存储在字符串照片那个的类名创建对象:

String s = "java,util.Random";
Object m = Class.forName(s).newInstance(); // 此处注意Employee必须有默认constructor
// or
Employee employee = (Employee)Class.forName(s).newInstance();

通过反射创建新的类实例,有两种方式:

(1) Class.newInstance()

​ 只能调用无参的构造函数,要求被调用的构造函数是public类型

(2) Constructor.newInstance()

​ 可以根据传入参数调用构造函数,在特定情况下被调用的构造函数可以是private类型

推荐使用Constructor.newInstance()

附:newInstance()分别调用无参和有参构造方法:
// Constructor.newInstance()
// 调用无参的构造方法
Employee employee = Employee.class.getDeclaredConstructor().newInstance();
System.out.println(employee.getName());

// 调用有参的构造方法(可调用private构造方法)
// 这一步代表调用某个已定义的有参构造方法
Constructor constructor = Employee.class.getDeclaredConstructor(new Class[]{String.class, int.class, int.class});
Employee obj2 = (Employee) constructor.newInstance(new Object[]{"james", 45, 30000});
System.out.println(obj2.getName());

捕获异常

异常有两种类型:未检查异常(null引用,数组越界)、已检查异常(提供异常处理器)

try {
    // statements that might throw exceptions
}
catch(Exception e) {
  // handler action
}

利用反射分析类的能力

Class类中的getFieldsgetMethodsgetConstructors方法分别返回类的public域、方法和构造器数组,其中包含超类的公有成员

Class类中的getDeclaredFieldsgetDeclaredMethodsgetDeclaredConstructors方法分别返回类中声明的全部域、方法和构造器,包含private和protected但不包含超类的公有成员!

上面一段话也反应了getXXX()getDeclaredXXX()的区别。

下面是一个反射分析类的例子:

import java.lang.reflect.*;
import java.util.Scanner;

public class ReflectionTest {

  public static void main(String[] args) {
    // 从args或input中读取className
    String name;
    if (args.length > 0) {
      name = args[0];
    } else {
      Scanner in = new Scanner(System.in);
      System.out.println("Enter Class Name(e.g. java.util.Date): ");
      name = in.next();
    }

    // 开始反射分析类的构造
    try {
      Class cl = Class.forName(name);
      // 获取父类的Class对象
      Class supercl = cl.getSuperclass();
      // 类的约束类型
      String modifiers = Modifier.toString(cl.getModifiers());
      if (modifiers.length() > 0) {
        System.out.print(modifiers + " ");
      }
      // 类名
      System.out.print("class " + cl.getName());
      // 是否有除Object外的超类继承
      if (supercl != null && supercl != Object.class) {
        System.out.print(" extends " + supercl.getName());
      }
      System.out.println("\n{\n");
      printConstructor(cl);
      System.out.println();
      printMethods(cl);
      System.out.println();
      printFields(cl);
      System.out.println("}\n");
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }

  }

  // 打印类的所有构造器
  public static void printConstructor(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 + "(");

      // 打印参数类型
      // Java中为何没有发射获得参数名的方法:传入参数只需要考虑采纳数类型
      Class[] paramTypes = c.getParameterTypes();
      for (int i = 0; i < paramTypes.length; ++i) {
        if (i > 0) {
          System.out.print(", ");
        }
        System.out.print(paramTypes[i].getName());
      }
      System.out.println(");");
    }
  }

  // 打印类的所有方法
  public static void printMethods(Class cl) {
    Method[] methods = cl.getDeclaredMethods();
    for (Method method : methods) {
      Class returnType = method.getReturnType();
      System.out.print(" ");
      String modifiers = Modifier.toString(method.getModifiers());
      if (modifiers.length() > 0) {
        System.out.print(modifiers + " ");
      }
      // 打印方法返回值
      System.out.print(returnType.getName()+ " ");
      System.out.print(method.getName() + "(");
      Class[] paramTypes = method.getParameterTypes();
      for (int i = 0; i < paramTypes.length; ++i) {
        if (i > 0) {
          System.out.print(", ");
        }
        System.out.print(paramTypes[i].getName());
      }
      System.out.println(");");
    }
  }

  // 打印所有域
  public static void printFields(Class cl) {
    Field[] fields = cl.getDeclaredFields();
    for (Field field : fields) {
      Class type = field.getType();
      String name = field.getName();
      System.out.print(" ");
      String modifiers = Modifier.toString(field.getModifiers());
      if (modifiers.length() > 0) {
        System.out.print(modifiers + " ");
      }
      System.out.println(type.getName() + " " + name + ";");
    }
  }
}

结果如下所示:

Enter Class Name(e.g. java.util.Date): 
java.lang.Double
public final class java.lang.Double extends java.lang.Number
{

 public java.lang.Double(double);
 public java.lang.Double(java.lang.String);

 public boolean equals(java.lang.Object);
 public static java.lang.String toString(double);
 public java.lang.String toString();
 public int hashCode();
 public static int hashCode(double);
 public static double min(double, double);
 public static double max(double, double);
 public static native long doubleToRawLongBits(double);
 public static long doubleToLongBits(double);
 public static native double longBitsToDouble(long);
 public volatile int compareTo(java.lang.Object);
 public int compareTo(java.lang.Double);
 public byte byteValue();
 public short shortValue();
 public int intValue();
 public long longValue();
 public float floatValue();
 public double doubleValue();
 public static java.lang.Double valueOf(java.lang.String);
 public static java.lang.Double valueOf(double);
 public static java.lang.String toHexString(double);
 public static int compare(double, double);
 public static boolean isNaN(double);
 public boolean isNaN();
 public static boolean isInfinite(double);
 public boolean isInfinite();
 public static boolean isFinite(double);
 public static double sum(double, double);
 public static double parseDouble(java.lang.String);

 public static final double POSITIVE_INFINITY;
 public static final double NEGATIVE_INFINITY;
 public static final double NaN;
 public static final double MAX_VALUE;
 public static final double MIN_NORMAL;
 public static final double MIN_VALUE;
 public static final int MAX_EXPONENT;
 public static final int MIN_EXPONENT;
 public static final int SIZE;
 public static final int BYTES;
 public static final java.lang.Class TYPE;
 private final double value;
 private static final long serialVersionUID;
}

在运行时用反射分析对象

我们先看一段代码:

Employee hacker = new Employee("hacker", 25, 20000);

    try {
      Class cl = hacker.getClass();
      Field f = cl.getDeclaredField("name");
      // the value of the name Field of the hacker object
      Object value = f.get(hacker); 
      System.out.println(value);  // IllegalAccessException!!
    } catch (NoSuchFieldException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }

会爆出如下错误:

java.lang.IllegalAccessException: Class ObjectAnalyzer can not access a member of class Employee with modifiers "private"
...

在Java的安全机制下,反射只允许查看任意对象有哪些域,而不能直接获取私有域的值。

如果想要达到此目的,需要关闭安全管理器(Field、Method or Constructor):

f.setAccessible(true);

它是AccessibleObject类中的一个方法,是Field、Method or Constructor类的公共超类。

class Field extends AccessibleObject implements Member {
...

假如读取的是基本类型的字段,get方法会自动将这个域打包到一个对象包装器中(自动装箱吧就是)

如age字段会打包到Integer中。

顾名思义,调用f.set(obj, value)可以设置相应的值

使用反射编写泛型数组

这一节主要是如果编写一个动态扩展任意类型数组的通用方法。

观察下面一段所谓扩展泛型数组的代码:

public static Object[] badCopyOf(Object[] a, int newLength) {
  Object[] newArray = new Object[newLength];
  System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
  return newArray;
}

看似可行,但是我们来运行:

// main
String[] b = {"tim", "jerry", "Dump"};
b = (String[]) badCopyOf(b, 10);

会抛出ClassCastException异常

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;

因为将一个String[]临时转成Object[],再转回来,都是正常的

但是如果一开始就是Object[]的数组是永远不能转换成String[]的。

对于以上论述,我实验了简单的例子:

public static void main(String[] args) {
  Employee employee = new Employee("jerry", 18,1000);
  Object obj = new Object();
  Object objFromChild = new Employee("child", 24, 2333);

  employee = (Employee) objFromChild;
  System.out.println(employee.getName());
  employee = (Employee) obj;
  System.out.println(employee.getName());
}

输出如下:

child

Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to Employee

可以看到一开始就用Employee对象初始化的的Object类型的objFromChild,它仍然能强制转换成Employee对象。而一开始初始化就是Object类型的obj对象则无法强制转换,抛出ClassCastException异常。

所以可以得出结论,上面的badCopyOf方法应用于泛型数组动态扩张是不行的。

关键在于我们需要在创建数组的时候就创建与原数组相同类型的新数组

这个时候就要用到反射(java.lang.reflect包)中的Array类的一些方法了。

我们创建一个goodCopyOf函数:

public static Object goodCopyOf(Object a, int newLength) {
  Class cl = a.getClass();
  if (!cl.isArray()) return null;

  // 先获得原数组的类型
  // Returns the {@code Class} representing the component type of an
  // array.  If this class does not represent an array class this method
  // returns null.
  Class componentType = cl.getComponentType();
  // 再获得原数组的长度
  int oldLength = Array.getLength(a);
  // 用Array的newInstance初始化生成新的数组对象
  Object newArray = Array.newInstance(componentType, newLength);
  // 将原来数组中的元素拷贝到新数组汇总
  System.arraycopy(a, 0, newArray, 0, Math.min(oldLength, newLength));
  return newArray;
}

这里返回值为什么要设置成Object而不是Object[]? 因为例如Object可以强制转换成int[], 而Object[]不行

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

调用任意方法

下面是一个用反射调用Method中相应方法的例子

对于java.lang.reflect.Method中invode的描述:

public Object invoke(Object implicitParamter, Object[] explicitParamter);

调用Method对象所描述的方法,并返回方法的返回值,对于静态方法把null作为隐式参数传递。

import java.lang.reflect.Method;

public class MethodTableTest {

  public static void main(String[] args) throws Exception {
    // 反射获取两个方法
    // 一个为MethodTableTest类的静态方法square
    // 另一个为Math类的静态方法sqrt
    Method square = MethodTableTest.class.getMethod("square", double.class);
    Method sqrt = Math.class.getMethod("sqrt", double.class);

    // 分别打印由square和sqrt生成的table
    printTable(1, 10, 10, square);
    System.out.println();
    printTable(1, 10, 10, sqrt);
  }

  public static double square(double x) {
    return x * x;
  }

  public static void printTable(double from, double to, int n, Method f) {
    System.out.println(f);
    double dx = (to - from) / n - 1;
    for (double x = from; x <= to; ++x) {
      try {
        // 调用Method对象f所描述的方法,并返回方法的返回值
        // 对于静态方法,把null作为隐式参数传递
        double y = (Double) f.invoke(null, x);
        System.out.printf("%10.4f | %10.4f\n", x, y);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}

输出:

public static double MethodTableTest.square(double)
    1.0000 |     1.0000
    2.0000 |     4.0000
    3.0000 |     9.0000
    4.0000 |    16.0000
    5.0000 |    25.0000
    6.0000 |    36.0000
    7.0000 |    49.0000
    8.0000 |    64.0000
    9.0000 |    81.0000
   10.0000 |   100.0000

public static double java.lang.Math.sqrt(double)
    1.0000 |     1.0000
    2.0000 |     1.4142
    3.0000 |     1.7321
    4.0000 |     2.0000
    5.0000 |     2.2361
    6.0000 |     2.4495
    7.0000 |     2.6458
    8.0000 |     2.8284
    9.0000 |     3.0000
   10.0000 |     3.1623
Last Modified: September 23, 2019
Archives QR Code
QR Code for this page
Tipping QR Code