Java程式設計思想(五)—— 初始化與清理
一、用構造器確保初始化
C++引入了構造器的概念,這是一個在建立物件時被自動呼叫的特殊方法。Java中也採用了構造器,並額外提供了“垃圾回收器”,對於不再使用的記憶體資源,垃圾回收器會自動將其釋放。
在Java中,通過提供構造器,類的設計者可確保每個物件都會得到初始化。建立物件時,如果其類具有構造器,Java就會在使用者有能力操作物件之前自動呼叫相應的構造器,從而保證了初始化的進行。
如何命名初始化的方法呢?有兩個問題:第一,所取的任何一個名字都可能跟類的某個成員重名;第二,呼叫構造器是編譯的責任,必須要讓編譯器知道應該呼叫哪個方法。所以,Java中採用了跟C++一樣的策略,即構造器採用與類相同的名稱。
public class Student {
Student(){
}
}
@Test
public void example1(){
Student stu1 = new Student();
}
在建立物件時,將會為物件分配記憶體空間,這就確保了你在操作物件之前,它已經被初始化了。不接受任何引數的構造器叫做預設構造器,Java中通常使用術語叫無參構造器。構造器是一特殊型別的方法,因為它沒有返回值。
構造器有利於減少錯誤,產使程式碼易於閱讀,從概念上講初始化與建立是彼此獨立的,但是在Java中,初始化和建立是捆綁在一起的,兩者不能分離。
二、方法過載
如果想用多種方式建立一個物件,這就需要多個構造器,但由於構造器的名字跟類名相同。為了讓方法名相同而形式引數不同的構造器同時存在,必須用到方法過載。既然構造方法可以過載,那麼過載也可適用其他方法。如下展示過載的構造方法:
public class Student { private String name; private Integer age; Student(){ } Student(String name, Integer age) { this.name = name; this.age = age; } }
1、區分過載方法
方法名相同的方法如何進行區分呢?那就是每個過載的方法都必須有一個獨一無二的引數型別列表。
2、涉及基本型別的過載
如果有某個過載方法接受int型引數,它會被直接呼叫,系統預設的是int型;如果傳入的資料型別小於方法中宣告的形式引數型別,實際資料型別就會被提升;char型略有不同,如果無法找到接受char引數的方法,就會把char直接提升到int型。如下:
public class BaseDataType {
void print1(short x){
System.out.println("print1 short");
}
void print1(int x){
System.out.println("print1 int");
}
void print1(char x){
System.out.println("print1 char");
}
void print1(double x){
System.out.println("print1 double");
}
void print2(short x){
System.out.println("print2 short");
}
void print2(int x){
System.out.println("print2 int");
}
void print2(double x){
System.out.println("print2 double");
}
void print3(double x){
System.out.println("print3 double");
}
}
@Test
public void example2(){
BaseDataType bdt = new BaseDataType();
byte b = 5;
bdt.print1(b);
bdt.print2(b);
bdt.print3(b);
/**
* print1 short
* print2 int
* print3 double
*/
bdt.print1('x');
bdt.print2('x');
bdt.print3('x');
/**
* print1 char
* print2 int
* print3 double
*/
}
如果傳入的實際引數較大,就得通過型別轉換來進行強制窄化處理,否則編譯器會報錯。
3、以返回值能區分過載方法嗎?
看如下示例:
void print4(){
}
int print4(){
return 1;
}
呼叫時可能是下面這樣:
print4();//返回 void
print4();//返回int但這裡不需要返回值
這時就無法區分過載方法,編譯器也無法判斷究竟應該呼叫哪個方法,別人閱讀時也無法理解。所以根據方法的返回值來區分過載是行不通的。
三、預設構造器
預設構造器是沒有形式引數的,它的作用是建立一個預設的物件,如果你的類中沒有構造器,那麼編譯器會自動幫你建立一個預設的構造器。但是,如果你已經定義了一個構造器(無論是否有引數),編譯器就不會幫你自動建立預設構造器。
沒有構造器,但依然可以建立物件,如下:
public class Student {
}
@Test
public void example1(){
Student stu1 = new Student();
}
四、this關鍵字
如下程式碼,如何知道peel()方便是被a還是被b呼叫呢?
public class BananaPeel {
public static void main(String[] args) {
Banana a = new Banana(),
b = new Banana();
a.peel(1);
b.peel(2);
}
}
class Banana{
void peel(int i){
//...
}
}
為了能用簡便、面向物件的語法來編寫程式碼(即傳送訊息給物件),編譯器做了一些幕後工作,它暗自把“操作物件的引用”作為第一個引數傳遞給peel(),所以它們的呼叫形式如下:
Banana.peel(a,1);
Banana.peel(b,2);
這裡其內部的表示形式,我們並不能這樣去書寫程式碼,它是由編譯器“偷偷”來決定的,並沒有識別符號可用,但有一個關鍵字來表示,即:this。this關鍵字只能在方法內部使用,表示對呼叫方法那個物件的引用。如果在方法內部呼叫同一個類的另一個方法,是不需要使用this的,直接呼叫即可。示例如下:
void peel1(){
this.peel(1);
}
void peel2(){
peel(1);
}
它們在效果上是一樣的,只是第二次呼叫省略了關鍵字this而已,在編譯時,編譯器會自動幫你加上。在實際程式設計中,如無必要一般會省略關鍵字,除非有一些需求明確指出對當前物件的引用時,才需要使用this關鍵字。如下示例,返回對當前物件的引用:
public class Leaf {
int i = 0;
Leaf increment(){//同樣功能使用this的寫法
i++;
return this;
}
Leaf increment(Leaf f){//同樣功能不使用this的寫法
f.i ++;
return f;
}
void print(){
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf f = new Leaf();
f.increment().increment().increment().print();
//i = 3
Leaf f2 = new Leaf();
f2.increment(f2).increment(f2).increment(f2).print();
//i = 3
}
}
由於increment()方法返回了對當前物件的引用,所以可以在一條語句裡對同一個物件進行多次操作,而且通過對比發現這一個功能使用this寫起來更簡潔。
1、在構造器中呼叫構造器
public class Flower {
int count = 0;
String s = "初始化一";
Flower(int i){
this.count = i;
}
Flower(String s){
//由於引數的別名跟屬性名相同,這裡需要使用this來指明呼叫資料成員
this.s = s;
}
Flower(int i,String s){
this(i);//使用this關鍵字呼叫一個構造器
this.s = s;
}
Flower(){
this(50,"初始化二");
}
public static void main(String[] args) {
Flower f1 = new Flower();
System.out.println("count = " + f1.count + ",s = " + f1.s);
//count = 50,s = 初始化二
Flower f2 = new Flower(100,"初始化三");
System.out.println("count = " + f2.count + ",s = " + f2.s);
//count = 100,s = 初始化三
}
}
可以使用this呼叫一個構造器,但卻不能同時呼叫兩個,而且必須將構造器置於當前方法的起始處,否則將編譯報錯。而且只有構造器才能呼叫構造器,編譯器禁止在當前類的其他方法中呼叫任何構造器。從上面的示例中,我們也看到了this關鍵字的另一種用法,就是在初始化資料成員時,可以使用this關鍵字來區分引數名跟資料成員名相同的情況。
2、static的含義
static方法就是沒有this的方法,由於沒有this,所以它不是通過“向物件傳送訊息”的方式來完成呼叫 的,因此你不能在靜態方法內部呼叫任何非靜態方法,但反過來可以,甚至你可以使用類名直接呼叫靜態方法,這是static方法的最主要用途。
五、清理:終結處理和垃圾回收
1、finalize()的用途何在
Java垃圾回收器負責釋放無用物件佔用記憶體資源,但由於垃圾回收器只知道釋放那些經由new分配的記憶體,如果你的物件並非使用new獲得了一塊特殊的記憶體區域,這裡它就不知道如何釋放該物件的這塊特殊記憶體。為了應對這種情況,java允許在類中定義一個finalize()的方法,它的工作原理是:一旦垃圾回收器準備好釋放物件佔用的儲存空間,將首先呼叫其finalize()方法,並且在下一次垃圾回收動作發生時,才會真正回收物件佔用的記憶體。
finalize()方法與C++中的解構函式的區別在於:
① 物件可能不會被垃圾回收;
② 垃圾回收並不等於“析構”;
③ 垃圾回收只與記憶體有關。即使用垃圾回收器的唯一原因是為了回收程式不再使用的記憶體,所以對於與垃圾回收有關的任何動作來說(尤其是finalize()方法),它們也必須同記憶體及其回收有關。
之所以要有finalize()方法,是由於在分配記憶體時採用了類似C語言的做法,而非Java中的通常做法。這種情況主要發生在使用“本地方法”的情況下,本地方法是在Java中呼叫非Java程式碼的方式。目前本地方法只支援C和C++,但它們可以呼叫其他語言寫的程式碼,所以實際上可以呼叫任何程式碼。在非Java程式碼中,也許會呼叫C的malloc()函式系列來分配儲存空間,而且除非呼叫了free()函式,否則儲存空間將得不到釋放,從而造成記憶體洩漏。free()是C和C++中的函式,所以需要在finalize()中用本地方法呼叫它。
2、你必須自己實施清理
由於垃圾收集機制的存在,使得Java中沒有解構函式,但垃圾回收器的存在並不能完全代替解構函式,如果希望進行除了釋放儲存空間之外的清理工作,使用者還是得明確呼叫某個恰當的Java方法,這就相當於解構函式,只是沒有解構函式方便。
其實,無論是“垃圾回收”還是“終結”,都不保證一定會發生,如果Java虛擬機器並未面臨記憶體耗盡的情形,它是不會浪費時間去執行垃圾回收以恢復記憶體的。所以我們不能去直接呼叫finalize()方法,而是要建立其他的清理方法,並且明確的呼叫它們。
注意:System.gc()是用來強制進行終結動作的。
3、垃圾回收器如何工作
⑴ Java虛擬機器的堆模型以及垃圾回收器在堆上的作用
在其他程式語言中,在堆上分配物件的代價十分高昂,但在Java中,垃圾回收器對於提高物件的建立速度,有明顯的效果,這意味著,Java從堆分配空間的速度,可以跟其他語言從堆疊上分配空間的速度相媲美。
在某些Java虛擬機器中,堆的實現像一個傳送帶,每分配一個新物件,它就往前移動一格,Java的堆指標只是簡單地的移動到尚未分配的區域,其效率比得上C++在堆疊上分配空間的效率。當然,在實際過程中在簿記工作方面還有少量的額外開銷,但比不上查詢可用空間開銷大。
其實,Java中的堆也未必完全像傳送帶那樣工作,如果真的是那樣的話,勢必會導致頻繁的記憶體頁面排程(將其移進移出硬碟),頻繁的頁面排程會顯著的影響效能,甚至耗盡記憶體資源。但由於有垃圾回收器的介入,垃圾回收器在工作時,將一面回收空間,一面使堆中的物件緊湊排列,這樣堆指標就可以很容易的移動到更靠近傳送帶的開始處,也就儘量的避免了頁面錯誤。通過垃圾回收器對物件進行重新排列,實現了一種高速的、有無限空間可供分配的堆模型。
⑵ 垃圾回收器的工作原理
垃圾回收器的工作思想:對於任何“活”的物件,一定能最終追溯到其存活在堆疊或靜態儲存區的引用,這個引用鏈條可能會穿過數個物件層次。由此,如果從堆疊或靜態儲存區開始,遍歷所有引用,就能找到所有“活”的物件。對於發現的每個引用,必須追蹤它所引用的物件,然後是此物件所包含的所有引用,如此反覆進行,直到“根源於堆疊和靜態儲存區的引用”所形成的網路被全部訪問為止。當然,你所訪問過的物件必須都是“活”的。
在這種工作思想下,Java虛擬機器採用的是一種自適應的垃圾回收技術,它分為兩種工作模式:
第一種工作模式叫做:停止——複製
它會先暫停程式的執行,然後將所有存活的物件從當前堆複製到另外一個堆,沒有被複制的全部都是垃圾。當物件被複制到新堆時,它們是一個挨著一個的,所以新堆保持緊湊排列,然後就可以按照前述的方法簡單、快速的分配記憶體了。當把物件從一處搬到另一處時,所有指向它的那些引用都必須修正。位於堆或靜態儲存區的引用可以直接被修正,但可能還有其他指向這些物件的引用,它們在遍歷的過程中才能被找到(可以想像成有個表格,可以將舊地址對映到新地址)。
這樣做會產生兩個問題。一是效率會降低,因為要在兩個堆中來回倒騰,從而得維護比實際需要多一倍的空間,某些Java虛擬機器對此問題的處理方式是:按需從堆中分配幾塊較大的記憶體,複製動作發生在這些大塊記憶體之間,如果物件較大,它會佔用單獨的塊;二是在於複製,程式進入穩定狀態後,可能只會產生少量垃圾,甚至沒有垃圾,這時如果依然將所有記憶體自一處複製到另外一處,顯然就會很浪費。為了避免這種情形,一些Java虛擬機器會進行檢查,如果沒有新垃圾產生,就會轉換到另外一種工作模式。
第二種工作模式叫做:標記——清掃
它也會先暫停程式的執行,它所依據的思路同樣是從堆疊和靜態儲存區出發,遍歷所有的引用,進而找出所有存活的物件。每當它找到一個存活物件,就會給物件設一個標記,這個過程中不會回收任何物件。只有標記工作完成的時候,清理動作才會開始。在清理過程中,沒有標記的物件將會被釋放,不會發生任何複製動作。所以剩下的堆空間是不連續的,垃圾回收器要是希望得到連續空間的話,就得重新整理剩下的物件。
綜上所述,在Java虛擬機器中,記憶體分配以較大的塊為單位。有了塊之後,垃圾回收器在回收的時候就可以往廢棄的塊裡複製物件了,每個塊都用相應的代數來記錄它是否還存活。如果塊在某處被引用,代數將會被增加,垃圾回收器將對上次回收動作之後新分配的塊進行整理,這對處理大量短命的臨時物件很有幫助。垃圾回收器會定期進行完整的清理動作,大型物件仍然不會被複制,只是其代數會增加,內含小型物件的那些塊則會被複制並整理。Java虛擬機器會進行監視,如果所有物件都很穩定,垃圾回收器的效率降低的話,就切換到“標記——清掃”方式。同時,Java虛擬機器會跟蹤“標記——清掃”的效果,要是堆空間出現很多碎片,就會切換回“停止——複製”方式。這就是垃圾回收的“自適應”技術。
六、成員初始化
Java盡力保證,所有變數在使用前都能得到恰當的初始化,對於方法的區域性變數,如果在使用前沒有進行初始化,編譯就會報錯。而對於類的資料成員,如果是基本資料型別,則會為其賦一個初始值,示例如下:
public class InitialValues {
private boolean bool;
private char c;
private byte b;
private short s;
private int i;
private long l;
private float f;
private double d;
private String str;
public void printValue(){
System.out.println("boolean " + bool);
System.out.println("char " + c);//char的值為0,所以顯示的會是空白
System.out.println("byte " + b);
System.out.println("short " + s);
System.out.println("int " + i);
System.out.println("long " + l);
System.out.println("float " + f);
System.out.println("double " + d);
System.out.println("String " + str);
}
public static void main(String[] args) {
InitialValues values = new InitialValues();
values.printValue();
}
}
列印結果如下:
如果想為某個變數賦初值,那麼在定義類的成員變數時直接為其賦值就好了(但C++中不能這麼做)。
七、構造器初始化
1、初始化順序
在類的內部,成員變數定義的順序決定了它們初始化的順序,它們將在任何方法(包括構造器)呼叫之前得到初始化。示例如下:
/**
* author Alex
* date 2018/10/29
* description 用於演示類的資料成員的初始化順序
*/
public class OrderOfInitialization {
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
}
}
class Demo1{
Demo1(){
System.out.println("初始化Demo1");
}
}
class Demo2{
private Demo1 demo10 = new Demo1();
Demo2(){
test();
demo11 = new Demo1();
System.out.println("初始化Demo2");
}
private Demo1 demo11 = new Demo1();
void test(){
System.out.println("測試");
}
private Demo1 demo12 = new Demo1();
}
列印結果如下:
2、靜態資料的初始化
靜態資料初始化時,初始化的順序是先靜態物件,而後是非靜態物件。靜態初始化只有在必要時刻才會進行,如果不引用這個類,那麼它不會被初始化,當靜態物件初始化之後,它不會再次進行初始化,而非靜態物件會進行再次初始化。其實,無論建立多少個此類的物件,靜態資料都只佔用一份儲存區域。注意:static不能用於區域性變數,因此它只能作用於域,如果一個域是靜態的基本型別域,且沒有進行初始化,它將會獲得基本型別的標準初始值;如果它是一個引用物件,那麼它的預設初始化值就是NULL。示例如下:
/**
* author Alex
* date 2018/10/31
* description 用於模擬靜態資料初始化
*/
public class StaticInitialization {
public static void main(String[] args) {
System.out.println("現在初始化一個Demo5");
new Demo5();
}
static Demo4 demo4 = new Demo4();
static Demo5 demo5 = new Demo5();
}
class Demo3{
Demo3(){
System.out.println("初始化Demo3");
}
}
class Demo4{
static Demo3 demo3 = new Demo3();
Demo4(){
System.out.println("初始化Demo4");
}
}
class Demo5{
Demo3 demo3 = new Demo3();
static Demo3 demo33 = new Demo3();
Demo5(){
System.out.println("初始化Demo5");
}
}
列印結果如下:
3、顯式的靜態初始化
Java允許將多個靜態初始化動作組織成一個特殊的靜態子句,有時也叫做靜態塊,它是一段跟在static關鍵字後面的程式碼,這段程式碼僅僅執行一次,當首次生成這個類的一個物件時或者首次訪問屬於這個類的靜態資料成員時,它將被初始化。示例如下:
/**
* author Alex
* date 2018/10/31
* description 用於模擬靜態塊的初始化
*/
public class StaticBlockInitialization {
public static void main(String[] args) {
Demo7.demo60.f();
}
}
class Demo6{
Demo6(){
System.out.println("初始化Demo6");
}
void f(){
System.out.println("成員方法f");
}
}
class Demo7{
static Demo6 demo60;
static Demo6 demo61;
static {
demo60 = new Demo6();
demo61 = new Demo6();
}
}
列印結果如下:
八、陣列初始化
陣列只是相同型別的、用一個識別符號名稱封裝到一起的一個物件序列或基本型別資料序列,它使用方括號[ ]來定義和使用。如下:
int[] arr;
1、陣列的三種初始化方法
編譯器不允許指定陣列的大小,當你建立了一個對陣列的引用,這時並沒有給陣列物件本身分配任何空間,為了給陣列分配相應的儲存空間,必須寫初始化表示式。對於陣列,初始化表示式可以出現在程式碼的任何地方,也可以使用一種特殊的初始化表示式,這種特殊的初始化是由一對花括號括起來的值組成的。在這種情況下,儲存空間的分配等價於使用new。示例如下:
public static void main(String[] args) {
//int型陣列進行特殊的初始化
int[] arr1 = {10,20,30,40,50};
//宣告一個int型陣列引用
int[] arr2;
arr2 = arr1;
for(int i=0;i<arr2.length;i++){
arr2[i] += 1;
}
for(int j=0;j<arr1.length;j++){
System.out.print(arr1[j]+" ");
}
System.out.println();
//列印結果如下:
//11 21 31 41 51
}
如上所示,在Java中允許將一個數組賦給另一個數組,這樣做其實只是複製了一個引用而已,當改變陣列2的值時,也改變了陣列1的值,因為它們的引用指向的是同一塊儲存空間。length是陣列的固有成員,它的計數是從0開始的,所以陣列的最大下標數是length-1。
如果在編寫程式時並不能確定在數組裡需要多少個元素,可以用new在數組裡建立元素進行初始化。示例如下:
public static void main(String[] args) {
example2();
}
static void example2(){
int[] arr;
//用47作為種子生成隨機數(使用種子同一次生成的隨機數是一樣的,但不同次是不一樣的)
Random random = new Random(47);
//生成0-10之間的int型隨機數設定為陣列的長度
arr = new int[random.nextInt(10)];
System.out.println("陣列length = " + arr.length);
System.out.println(Arrays.toString(arr));
//列印結果如下:
//陣列length = 8
//[0, 0, 0, 0, 0, 0, 0, 0]
}
注意:先宣告arr,再使用arr = {}方式進行初始化是不允許的,只能在宣告的同時進行初始化才可以使用int[] arr={}的形式,當然在宣告的時候使用new int[]的方法進行初始化也是可以的,但必須指明陣列的長度,如:int[] arr = new int[10],如果不指明長度,編譯就會報錯。示例如下:
public static void main(String[] args) {
example3();
}
static void example3(){
int[] arr = new int[10];
Random random = new Random(47);
for(int i=0;i<arr.length;i++){
arr[i] = random.nextInt(100);
}
System.out.println("陣列length = " + arr.length);
System.out.println(Arrays.toString(arr));
//列印如果如下:
//陣列length = 10
//[58, 55, 93, 61, 61, 29, 68, 0, 22, 7]
}
//這種初始化方法跟上面一種是一樣的
下面來看看第三種初始化方式,其實是第一種和第二種結合的方式。示例如下:
public static void main(String[] args) {
example4();
}
static void example4(){
int[] arr = new int[]{10,20,30,40,50};
System.out.println("陣列length = " + arr.length);
System.out.println(Arrays.toString(arr));
}
//列印如果如下:
//陣列length = 5
//[10, 20, 30, 40, 50]
2、可變引數列表
有了可變引數,就可以不用顯式的編寫陣列語法了,當你指定引數時,編譯器實際上會為你去填充陣列,你獲取的仍然是一個數組,所以可以使用foreach進行迭代。可變引數列表主要用於在不可變的引數後面有可選的尾隨引數。在可變引數列表中可以使用任何型別的引數,包括基本資料型別。示例如下:
/**
* author Alex
* date 2018/11/1
* description 用於演示可變引數列表
*/
public class VariableParams {
public static void main(String[] args) {
VariableParams var = new VariableParams();
//可變引數列表
var.example2();
var.example2(1, 2, 3);
var.example2(10, 25.5, 'c', "測試");
//列印結果如下:
//1 2 3
//10 25.5 c 測試
//過載有可變引數列表的函式
var.example3("one","two");
var.example3(1,"one","two");
//列印結果如下:
//one two
//1 one two
System.exit(0);
}
void example2(Object... args) {
for (Object obj : args) {
System.out.print(obj + " ");
}
System.out.println();
}
void example3(int i,String... args){
System.out.print(i + " ");
for (String str:args){
System.out.print(str + " ");
}
System.out.println();
}
void example3(String... args){
for (String str:args){
System.out.print(str + " ");
}
System.out.println();
}
}
九、列舉型別
列舉類使用enum作為關鍵字,由於列舉類型別的例項是常量,因此按照命名慣例它們都是大寫字母表示。同時編譯器會自動建立ordinal()方法和values()方法用於操作列舉類。示例如下:
/**
* author Alex
* date 2018/11/1
* description 使用一星期來演示列舉類的使用
*/
public enum WeekEnum {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
public static void main(String[] args) {
//編譯器自動建立的方法
//static方法values()可以獲取列舉類的所有列舉值
//方法ordinal()可以獲取當前列舉物件的索引
WeekEnum[] values = WeekEnum.values();
for(WeekEnum weekEnum:values){
System.out.print(weekEnum.ordinal() + " " + weekEnum + " ");
}
System.out.println();
//列印結果如下:
//0 MONDAY 1 TUESDAY 2 WEDNESDAY 3 THURSDAY 4 FRIDAY 5 SATURDAY 6 SUNDAY
}
}
我們還可以在列舉類中定義自己想要的關於列舉物件的描述或其他附加資訊。示例如下:
/**
* author Alex
* date 2018/11/1
* description 使用一星期來演示列舉類的使用
*/
public enum WeekEnum {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");
private String notes;
WeekEnum(String notes) {
this.notes = notes;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public static void main(String[] args) {
WeekEnum friday = WeekEnum.FRIDAY;
switch (friday.ordinal()){
case 0:
System.out.println("今天" + friday.getNotes());
break;
case 1:
System.out.println("今天" + friday.getNotes());
break;
case 2:
System.out.println("今天" + friday.getNotes());
break;
case 3:
System.out.println("今天" + friday.getNotes());
break;
case 4:
System.out.println("今天" + friday.getNotes());
break;
case 5:
System.out.println("今天" + friday.getNotes());
break;
case 6:
System.out.println("今天" + friday.getNotes());
break;
}
}
}