Java学习笔记之面向对象

主要是记录Java中与C++不同的地方,快速浏览,参考书是《Java核心技术 卷I》
Java 8 SE API:https://docs.oracle.com/javase/8/docs/api/

1.Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用,也就是说Java中的对象实现上并不包含其对象本身,而只是指向一个别处的对象,类似于C++中的对象指针,与C++中的引用不同,C++中引用不能被赋值,创建时必须初始化。如:

1
Date birthday;   //java

类似于C++中的:

1
Date *birthday;  //c++

而java中的对象可以只创建不初始化,而在创建后使用new进行初始化,或者将其赋值为null。当然不初始化之类的变量无法使用。

Java内置类

2.Java类库中包含了大量实用的类,例如表示日期和时间的GregorianCalendar类(java.util.GregorianCalendar)。Date类表示一个特定的时间,即用距离一个固定时间(纪元 1970,01,01,00:00:00)的毫秒数表示。而GregorianCalendar类是用日历表示时间,一个new GregorianCalendar()对象表示对象构造时的日期和时间,或者表示某个特定的日期:new GregorianCalendar(1999,11,31);//表示12月,因为月份从0开始,或者使用常量并将其存储在对象变量中:GregorianCalendar deadline = new GregorianCalendar(1999, Calendar.DECEMBER, 32, 23, 59, 59);

3.GregorianCalendar类中包含了大量用于操作日期和时间的函数,其中get方法和set方法是最常用的,分别用于获取和设置其对象中的相应时间值,包括年、月、日,而Calendar类中定义了一些日期相关的常量,可用于表达希望从GregorianCalendar对象中的项,例如:

1
2
3
4
5
6
7
8
9
GregorianCalendar now = new GregorianCalendar();
int month = now.get(Calendar.MONTH); //获取月份
int weekday = now.get(Calendar.DAY_OF_WEEK); //获取周天
//使用set
now.set(Calendar.YEAR, 2016);
now.set(Calendar.MONTH, Calendar.APRIL);
now.set(Calendar.DAY_OF_MONTH, 15);
//使用add来为日期增加天数
now.add(Calendar.MONTH, 6); //当时日期向后移6个月,可以是负数,表示前移

java中类似get仅访问实例域而不进行修改的方法称为访问器方法(accessor method),类似set的对实例域做出修改的方法称为更改器方法(mutator method)。
类似C++中带有const后缀的方法是访问器方法,默认为更改器方法。但java中这两类方法语法上没有明显区别。通常的习惯是访问器方法名前面加上前缀get,更改器方法前面加上前缀set,例如下例

4.GregorianCalendar类的getTime方法和setTime方法,可以用来获得和设置日历对象所表示的时间点,利用它们可以实现Date类内容与GregorianCalendar类的转换,因为Date类不知道如何操作日历。例如,假定已知年、月、日并希望创建一个包含这个时间值的Date对象,就可以先创建一个GregorianCalendar对象,然后调用getTime方法获得Date对象时间:

1
2
3
4
5
6
//通过年、月、日创建Date对象
GregorianCalendar calendar = new GregorianCalendar(year, month, day);
Date time = clanedar.getTime();
//获取Date对象中的年、月、日
calendar.setTime(time);
int year = calendar.get(Calendar.YEAR);

5.下面这个例子将打印输出当前月的日历,支持指定以周几为一周的第一天,并在当前日期后面加个*号标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.util.*;  //用于输出及时间相关类
import java.text.DateFormatSymbols; //用于获取时间格式的字符串表示形式

public class CalendarTest {

public static void main(String[] args) {
//Locale.setDefault(Locale.CHINA); //设置地区为中国,这样会以星期-、星期二...的方式显示
//创建当前日期
GregorianCalendar d = new GregorianCalendar();
int today = d.get(Calendar.DAY_OF_MONTH);
int month = d.get(Calendar.MONTH);

//将日期设置为当月的第一天,并获取这一天为周几
d.set(Calendar.DAY_OF_MONTH, 1);
int weekday = d.get(Calendar.DAY_OF_WEEK);

//获取当前时区下每周的第一天为周几
//int firstDayOfWeek = d.getFirstDayOfWeek(); //根据地区设定
int firstDayOfWeek = 2; //强制修改为周一为第一天

//确定打印日历的第一行所需要的缩进
//通过将当前日期减-1,直到当前日期为本周的第一天停止
int indent = 0;
while (weekday != firstDayOfWeek)
{
indent++;
d.add(Calendar.DAY_OF_MONTH, -1);
weekday = d.get(Calendar.DAY_OF_WEEK);
}

//打印第一行的周名称
//此时d中存储的周日期即为本周的第一天,所以通过将其日期加1,逐个打印即可
String[] weekdayNames = new DateFormatSymbols().getShortWeekdays(); //获取一周的每天的缩写
do
{
System.out.printf("%4s", weekdayNames[weekday]); //每个周名称占4个字符空间
d.add(Calendar.DAY_OF_MONTH, 1);
weekday = d.get(Calendar.DAY_OF_WEEK);
}
while (weekday != firstDayOfWeek);
System.out.println();
//打印日期
for (int i = 0; i < indent; ++i)
System.out.print(" ");
d.set(Calendar.DAY_OF_MONTH, 1);
do
{
int day = d.get(Calendar.DAY_OF_MONTH);
System.out.printf("%3d", day);

//将当前日期后面使用*标记
if (day == today) System.out.print("*");
else System.out.print(" ");

//将日期指向下一天
d.add(Calendar.DAY_OF_MONTH, 1);
weekday = d.get(Calendar.DAY_OF_WEEK);

//新的一周开始时换行
if (weekday == firstDayOfWeek) System.out.println();
}
while (d.get(Calendar.MONTH) == month); //当月份变成下个月时退出循环

//如果最后当前d中的日期不是一周的第一天,则需要换行
if (weekday != firstDayOfWeek) System.out.println();
}

}

6.java.util.GregorianCalendar的API中包含大量日期时间相关的方法,java.text.DateFormatSymbols包的API中包含很多关于日期的字符串表示形式,如获得当前地区的星期几或月份的名称缩写等:String[] getShortWeekdays(); String[] getShortMonths()

自定义类

1.java中文件名必须与public修饰的类名相同,一个文件中只能有一个public类,但可以有任意数目非公有类。如果将2个类放在不同的文件中,且公有类使用了另一个类,例如文件名都有前缀Employ,则可以这样编译:javac Employ*.java。或者直接使用javac编译那个公有类,当时java编译器发现这个类引用了另一个类时,会去查找其class文件,如果没有找到,或者发现其java文件较已有的class文件版本新,java编译器就会自动地重新编译这个文件。使用java解释器运行时,只需指定带有main方法的公有类的class文件即可。
当一个文件中有两个不同的类的时候,编译之后会生成两个类名的class文件,将带有main方法的class传递给java解释器运行即可。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class EmployeeTest {
public static void main(String[] args) {
Employee blueyi = new Employee("blueyi", 27);
System.out.println(blueyi.getName() + " : " + blueyi.getAge());
}
}

class Employee {
private String name = "";
private int age = 0;
public Employee(String n, int a) //构造函数
{
name = n;
age = a;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
}

编译之后将会有一个EmployeeTest.class文件和Employee.class文件

2.Java构造器的工作方式与C++一样,但java中的对象都是在堆中构造的,所以必须与new操作符一起使用。不能在构造器中定义与实例域重名的局部变量。

3.java中类方法的第一个参数也是隐式的this,但这个this是类对象,而不是指针。java的所有类方法都必须在类的内部定义,而不能像C++那个类外定义。

4.不要编写返回引用可变对象的访问器方法,由于java中返回的对象默认是引用,所以使用访问器返回的引用会修改原私有数据,可以通过返回克隆的方法进行数据域的拷贝,这样就可以保护原对象中的私有数据:

1
2
3
4
5
6
7
class Employee
{
public Date getHireDay()
{
return hireDay.clone();
}
}

方法可以访问所属类的所有对象的私有数据

5.final实例域,可以使用final修鉓类中的实例域,被final修饰的数据必须在构造器中设置,并且在后面的操作中不能对它修改,final修饰符大都应用于基本(primitive)类型域,
或不可变(immutable)类的域,如String类,例如:

1
2
3
4
class Employee
{
private final String name;
}

6.java中的static作用与C++中的类似,表示属于类且不属于类对象的变量和方法。由于static方法没有隐式的this对象,所以不能访问对象的非static方法及数据,但可以访问该类的其他static域。
static方法调用与C++不同的是直接使用.运算符,而不是::,如:Math.pow

7.可以通过为每一个类添加一个main方法用于单元测试该类的功能,例如StaticTest类名与文件名相同,其中又包含了一个名为Employee的类,例如两个类中都有main方法,
那么运行:java Employeejava StaticTest将执行两个不同的main方法。

8.java中的函数参数传递方式都是按值调用。只是当传递的是对象的时候,传递的是对象的引用的拷贝(即实际上还是按值传递),所以可以通过方法修改对象,但却不能修改基本数据类型的参数。实际上C++中当向函数传递一个指针的时候传递也是指针的拷贝。总结来说java中的方法参数使用情况:

  • 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)。
  • 一个方法可以改变一个对象参数的状态。(即可以通过方法修改对象中的参数)
  • 一个方法不能让对象参数引用一个新的对象。(例如不能通过向方法传递2个对象来交换它们)

9.Java支持函数重载,如果类的域没有在构造器中显式地赋予初值,则会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。但同样建议要手动进行初始值初始化。

10.只有当类没有提供任何构造函数时,编译器才会提供一个默认的无参数构造函数。如果提供了一个有参数的构造函数,则此时不会提供无参数的构造函数,
如果调用new创建无参数对象时,默认就会调用无参数构造器,此时就会在编译时报错。

11.Java中会在执行构造器之前,先执行赋值操作,所以可以通过下面的方式对域进行初始化:

1
2
3
4
5
6
7
8
9
10
11
class Employee
{
private static int nextId;
private int id = assignId();
private static int assignId()
{
int r = nextId;
nextId++;
return r;
}
}

12.构造器的实例域在命名时可以加上前缀来区分,或者实例域使用原名字,而通过在构造器的内部使用this来引用类变量的方式赋值:

1
2
3
4
5
6
7
8
9
10
11
public Employee(String aName, double aSalary)
{
name = aName;
salary = aSalary;
}
//或者
public Employee(String name, double salary)
{
this.name = name;
this.salary = salary;
}

13.可以通过this在一个构造器中调用另一个构造器:

1
2
3
4
5
6
public Employ(double s)
{
//call Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}

14.除了可以通过构造器在设置值和在声明中赋值来进行初始化数据域外,还可以通过初始化块(initialization block)来构造类的对象,只要构造类的对象,这些初始化块就会被执行。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Employee
{
private static nextId;
private int id;
private String name;
// object initialization block
{
id = nextId;
nextId++;
}

public Employee(String n, int i)
{
name = n;
id = i;
}
}

15.初始化数据域有多种途径,调用顺序如下:

  • 所有数据域被初始化为默认值:0、false或null
  • 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块
  • 如果构造器第一行调用了第二个构造器,则执行第二个构造器的主体
  • 执行这个构造器的主体

16.也可以将初始化块标记为静态域,在类的第一次加载的时候,将进行静态域的初始化:

1
2
3
4
5
6
7
public class Hello
{
static
{
System.out.println("Hello, World");
}
}

执行这段程序将打印输出Hello, World,而无需main方法。

17.java中有垃圾回收来自动处理内存回收问题,而无需析构函数,但可以添加finalize方法,finalize方法将在垃圾回收器清除对象之前调用。

java中使用包(package)将类组织起来,标准的java类库分布在多个包中,包括java.lang、java.util和java.net等,都具有一个层次结构。
使用包主要是确保类名的唯一性,Sun公司建议将公司的互联网域名以逆序的形式作为包名,并且对于不同的项目使用不同的子包。
例如我的域名maxwi.com,逆序就是com.maxwi,如果笔记作为一个包就是com.maxwi.notes。有点类似于C++中的命名空间

类的导入

1.一个类可以使用所属包中的所有类,以及其他包中的公有类(public class),有两种方式使用另一包中的公有类,一种是使用完整的包名,
如:java.util.Date today = new java.util.Date();,另一种是使用import,import即可以一次导入一个特定的类,也可以导入整个包,
import应该位于源文件的顶部,但位于package语句的后面:

1
2
import java.util.*; //导入java.util包中的所有的类
import java.util.Date; //导入Date类

2.如果导入的2个包都同时含有同一个类,则会出现编译错误,可以通过添加一个特定的import来指定使用哪个类来解决:

1
2
3
import java.util.*;
import java.sql.*;
import java.util.Date; //util和sql中都包含有类Date

当需要使用sql中的类Date时,可以直接使用完整的包名来引用

3.import也可以用来导入静太方法:

1
2
import static java.lang.System.*;
out.println("Hi");

包的使用

如果不手动指定文件中的类所属的包的话,会默认在default包中
4.想将类放入包中只需要在文件的开头使用package加上包名即可,此时要注意类文件必须在以包名指定的以.分隔的路径下,如:

1
2
3
4
5
package com.maxwi.notes;  //表示该类将在路径com/maxwi/notes/Employee.java
public class Employee
{
...
}

编译方法是在包的根目录或者同一个目录下的其他子目录,如果使用另一个java文件调用该类,则编译这个调用类的文件即可:
类Employee路径com/maxwi/notes/Employee.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.maxwi.notes;

public class Employee
{
private String name = "";
private int age = 0;
public Employee(String n, int a) {
name = n;
age = a;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
}

类TestEmployee所在路径:com/testEmployee/TestEmployee.java

1
2
3
4
5
6
7
8
9
10
package com.testEmployee;
import com.maxwi.notes.*;

public class TestEmployee
{
public static void main(String[] args) {
Employee blueyi = new Employee("blueyi", 27);
System.out.println(blueyi.getName() + " : " + blueyi.getAge());
}
}

需要从根目录编译带有main方法的类

1
javac com/testEmployee/TestEmployee.java

运行只需要在根目录下执行带包的类名即可,解释器会自动查找目录下的class文件:

1
java com.testEmployee.TestEmployee

目录结构如下:

1
2
3
4
5
6
7
8
9
10
E:.
└───com
├───maxwi
│ └───notes
│ Employee.class
│ Employee.java

└───testEmployee
TestEmployee.class
TestEmployee.java

5.被public修饰的部分可以被任意的类使用,被private修饰的部分只能被定义它的类使用,而没有指定public或private的部分(类、方法或变量)只可以被同一个包中的所有方法访问。
具体访问权限参见后面继承中的说明。

6.类路径必须与包名匹配,另外类文件也可以存储在JAR(Java归档)文件中,JAR文件就是包含目录结构的java文件,为了使类能够被多个程序共享,
首先必须将类以包名指定的目录结构存放,或者直接是一个JAR文件中,然后将其加入到类路径(class path)中,
类路径就是包含类文件的路径的集合,类路径默认不包含当前路径,即点,所以通常也要将当前目录加入类路径。UNIX环境中类路径以:分隔,windows下以分号;分隔。
在编译时通过选项-classpath-cp来指定类路径:

1
java -classpath /home/usr/classdir:.:/home/usr/archive/archive.jar  MyProg # linux系统

或者通过设置CLASSPATH环境变量的方法来实现。

javadoc注释

可以在源文件中使用/*…./进行文档注释,然后使用javadoc工具生成html类型的类文档,文档注释应该放在所描述特性的前面,例如类前面,类方法前面等,注释文档中可以有以@开始的标记,如@author、@param、@return等,或者是自由格式文本,也可以包含html标签用于强调等,如<em>...</em>

类注释必须放在import语句之后,类定义之前

生成javadoc:

1
2
javadoc -d docDirectory nameOfPackage  #对包生成
javadoc -d docDirectory *.java #对java文件生成,如果省略-d docDirectory选项,会默认生成在当前目录

类设计技巧

  • 一定要保证数据私有
  • 一定要对数据初始化
  • 不要在类中使用过多的基本类型
  • 不是所有的域都需要独立的域访问器或域更改器
  • 将职责过多的类进行分解
  • 类名和方法名要能够体现它们的职责

继承

类与子类

1.反射(reflection)是指在程序运行期间发现更多的类及其属性的能力。
2.Java中的继承是使用关键字extends代替C++中的冒号(:)。在java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承。例如:

1
2
3
4
5
6
7
8
class Manager extends Employee
{
private double bonus;
public void setBonus(double b)
{
bonus = b;
}
}

3.派生类不能够直接访问超类(父类)的私有域,需要借助关键字super,同样派生类的构造函数也不能直接调用超类的构造函数,需要借助super关键字。例如定义Manager的getSalary方法时需要调用其父类的方法返回值+其bonus参数后返回,则需要如下方法调用:

1
2
3
4
5
public double getSalary()
{
double baseSalary = super.getSalary(); //相当于C++中的Employee::getSalary();
return baseSalary + bonus;
}

构造器的应用如下:

1
2
3
4
5
public Manager(String n, double s, int year, int month, int day)
{
super(n, s, year, month, day);
bonus = 0;
}

相当于C++中的:

1
2
3
4
Manager::Manager(std::string n, double s, int year, int month, int day) : Employee(n, s, year, month, day)
{
bonus = 0;
}

在java中,不需要将方法声明为虚方法,动态绑定是默认发生的,即父类定义的变量可以调用子类对象中的方法。java中的对象变量都是多态的,一个父类变量即可以引用其父类对象,也可以引用其任意一个子类的任何对象。但父类的引用不能赋给子类变量(与C++的多态类似)。如果不希望让一个方法具有虚拟特征,可以将它标记为final。

4.final修饰的类将不允许被继承,当类中的方法被声明为final时,则该方法在子类中就不能被覆盖。final类中的所有方法都自动地被成为final方法。

5.可以通过instanceof运算符在进行类型转换之前测试是否可以转换:

1
2
3
4
5
if (x instanceof C)
{
C ax = (C) x;
...
}

6.包含一个或多个抽象方法的类必须被声明为抽象的,类即使不含抽象方法,也可以将类声明为抽象类。抽象类不能被实例化,即使用abstract修饰的类不能创建其类对象,但可以创建一个抽象类的对象变量,并用它引用非抽象子类的对象:

1
2
3
4
5
abstract class Person //抽象类
{
public abstract String getDescription();
}
Person p = new Student("blueyi", 22); //抽象类对象p引用其子类的对象

7.Java中的访问保护:

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

详细信息如下:

修饰符 类内部 同一包 子类 任何类
private Yes - - -
default Yes Yes - -
protected Yes Yes Yes -
public Yes Yes Yes Yes

Object类