【Java】学习笔记1——从小小白到小白 (基础知识篇)里记录了Java中最最基础的知识,在对基础知识有了基本了解之后,就可以开始着手技术提升了。本篇博客也将延续第一篇,继续记录我的Java学习历程和心得体会。
————————————————————————————————————————————————————
类的继承:
Java中通过extends关键字来标识两个类的继承关系。子类可以调用父类的public和protected成员,其通过super关键字调用。在子类中使用super既可以调用父类的成员方法和变量,也可以写成super()来显式调用父类的构造方法。当然,继承除了可以扩展父类功能,还可以重写父类的成员方法。即保留父类的成员方法名称,仅仅重写成员方法的内容或改变权限、返回值等。
继承中还有一种特殊的形式——重构。这种情况下的子类与父类的成员方法返回值、方法名称、参数类型及个数完全相同,唯一不同的是方法内容,某种意义上可以将重构看成高级重写。
在重写父类的方法时,我们可以修改方法权限。但需要注意的是,权限只能从小范围改成大范围,即private可以修改为protected或public,protected可以修改为public,其余情况则是错误重写。
除了修改方法权限需要特别注意之外,修改返回值类型也需要注意一点:重写的返回值类型必须是父类中同一方法返回值的子类。
在实例化子类对象的时候,首先会调用父类的构造方法,再调用子类的构造方法。父类的无参构造方法会被自动调用,而有参构造方法不能被自动调用,这个时候只能使用super关键字进行显式调用父类的构造方法。
下面我们根据一个程序来更好地理解Java中类的继承:
public class A //父类A
{
public A() //父类构造方法
{
System.out.println("成功调用A类的构造方法。");
}
protected void dosomething() //父类成员方法
{
System.out.println("成功调用A类的dosomething()方法。");
}
protected A fun() //返回值为A类型
{
System.out.println("成功调用A类的fun()方法。");
return new A();
}
}
class B extends A //子类B,继承于父类A
{
public B() //子类构造方法
{
super();
System.out.println("成功调用B类的构造方法。");
}
public void dosomething() //重写父类方法,改变方法权限
{
System.out.println("成功调用B类的dosomething()方法。");
}
protected B fun() //重写父类方法,改变返回类型为B
{
System.out.println("成功调用B类的fun()方法。");
return new B();
}
public void new_dosomething() //子类新增方法
{
System.out.println("成功调用B类的new_dosomething()方法。");
}
public static void main(String args[]) //测试用主方法
{
//测试A类:
A a=new A(); //实例化父类对象a
a.dosomething();
a.fun();
System.out.println();
//测试B类:
B b=new B(); //实例化子类对象b
b.dosomething();
b.fun();
b.new_dosomething();
}
}
程序输出:
成功调用A类的构造方法。
成功调用A类的dosomething()方法。
成功调用A类的fun()方法。
成功调用A类的构造方法。
成功调用A类的构造方法。
成功调用B类的构造方法。
成功调用B类的dosomething()方法。
成功调用B类的fun()方法。
成功调用A类的构造方法。
成功调用B类的构造方法。
成功调用B类的new_dosomething()方法。
Object类:
Object类非常特殊,它是所有类的父类,可以理解为祖宗类。我们在创建一个类时,除非是指明了从其他类继承,否则其就是从java.lang.Object类继承而来的。由于所有的类都是它的子类,所以在定义类的时候可以省略extends Object。
Object类中主要包括clone()、finalize()、equals()、toString()等方法,任何类都可以重写这些方法。但并不是Object类中所有的方法都能被重写,像getClass()、notify()、notifyAll()、wait()等被定义为final的方法则不能被重写。下面我们来看看Object类中两个主要且常用的方法:
1.getClass()方法
getClass()方法会返回对象执行时的Class实例,然后根据这个实例调用getName()方法以取得类的名称,有点像调取类的ID。
还是用上面A、B类的程序举例,在主方法中再添加如下语句:
//测试getClass()方法
System.out.println(b.getClass());
System.out.println(b.getClass().getName());
程序输出:
class B
B
2.toString()方法
toString()方法的功能是将一个对象返回为字符串形式,即返回一个String类型的实例,我们可以重写这个方法来实现特定输出形式。
还是用上面A、B类的程序举例,在B类和主方法中再添加如下语句:
public String toString()
{
return "成功调用并重写toString()方法。";
}
//测试toString()方法
System.out.println(new B()); //打印本类对象
程序输出:
成功调用A类的构造方法。
成功调用B类的构造方法。
成功调用并重写toString()方法。
类型转换:
Java中经常需要对象类型的转换,其中主要包括向上转型和向下转型。所谓向上转型,就是将子类对象看作是父类对象;反之,向下转型就是将父类对象看作是子类对象。向下转型必须使用显式类型转换。
下面我们通过四边形类和平行四边形类来体会向上和向下转型:
public class Quadrangle //四边形类
{
Quadrangle()
{
System.out.println("成功调用四边形类的构造方法。");
}
public static void fun(Quadrangle q)
{
System.out.println("成功调用四边形类的fun()方法。");
}
}
class Parallelogram extends Quadrangle //平行四边形类,是四边形的子类
{
Parallelogram()
{
System.out.println("成功调用平行四边形类的构造方法。");
}
public static void main(String args[])
{
//测试向上转型
Parallelogram p1=new Parallelogram(); //实例化平行四边形类对象引用
fun(p1); //向上转型,把p看做是四边形类的对象,并调用四边形类的fun()方法
System.out.println();
//测试向下转型
Quadrangle q=new Parallelogram(); //向下转型,把q看作是平行四边形类的对象
Parallelogram p2=(Parallelogram) q; //将父类对象赋予给子类对象,需要强制转换为子类型,否则会报错
fun(p2);
fun(q);
}
}
程序输出:
成功调用四边形类的构造方法。
成功调用平行四边形类的构造方法。
成功调用四边形类的fun()方法。
成功调用四边形类的构造方法。
成功调用平行四边形类的构造方法。
成功调用四边形类的fun()方法。
成功调用四边形类的fun()方法。
当进行向下转型的时候,如果父类对象不是子类对象的实例,就会报错。所以在向下转型之前要先用instanceof操作符判断一个实例对象是否属于一个类。
class Quadrangle //四边形类
{
Quadrangle()
{
System.out.println("成功调用四边形类的构造方法。");
}
public static void fun(Quadrangle q)
{
System.out.println("成功调用四边形类的fun()方法。");
}
}
class Square extends Quadrangle //正方形类
{
Square()
{
System.out.println("成功调用正方形类的构造方法。");
}
}
class Triangle //三角形类,不是四边形类的子类
{
Triangle()
{
System.out.println("成功调用三角形类的构造方法。");
}
}
public class Parallelogram extends Quadrangle //平行四边形类
{
Parallelogram()
{
System.out.println("成功调用平行四边形类的构造方法。");
}
public static void main(String args[])
{
Quadrangle q=new Quadrangle(); //实例化父类对象
if(q instanceof Parallelogram) //判断父类对象是否为子类的一个实例
{
Parallelogram p=(Parallelogram) q; //向下转型
}
if(q instanceof Square) //判断父类对象是否为子类的一个实例
{
Square s=(Square) q; //向下转型
}
/*
if(q instanceof Triangle) //会报错
{
Triangle t=(Triangle) q;
}
*/
}
}
程序输出:
成功调用四边形类的构造方法。
方法的重载:
方法的重载就是在同一个类中允许存在多个同名方法,只要其参数个数、类型、顺序不同即可。需要注意的是,只有返回类型不同并不足以区分两个方法的重载,就这点而言,Java的方法重载和C++的函数重载规则大体相同。Java还能够进行不定长参数的方法重载,即在参数列表中使用“…”定义不定长参数,其中的不定长参数会被视作一个数组。
为了更好地理解方法的重载,可参考下面重载add()方法的程序:
public class OverLoadTest
{
public static int add(int a,int b)
{
return (a+b);
}
public static double add(double a,double b)
{
return (a+b);
}
public static double add(int a,double b)
{
return (a+b);
}
public static double add(double a,int b)
{
return (a+b);
}
public static int add(int a)
{
return a;
}
public static double add(double a)
{
return a;
}
public static int add(int...a) //不定长参数重载,将a看做数组
{
int sum=0,i;
for(i=0;i<a.length;i++)
{
sum+=a[i];
}
return sum;
}
public static void main(String args[])
{
System.out.println("调用add(int,int)方法,计算1+2:"+add(1,2));
System.out.println("调用add(double,double)方法,计算1.1+2.8:"+add(1.1,2.8));
System.out.println("调用add(int,double)方法,计算1+2.8:"+add(1,2.8));
System.out.println("调用add(double,int)方法,计算1.1+2:"+add(1.1,2));
System.out.println("调用add(int)方法:"+add(1));
System.out.println("调用add(double)方法:"+add(2.8));
System.out.println("调用add(int...)方法:计算1加到9的和:"+add(1,2,3,4,5,6,7,8,9));
}
}
程序输出:
调用add(int,int)方法,计算1+2:3
调用add(double,double)方法,计算1.1+2.8:3.9
调用add(int,double)方法,计算1+2.8:3.8
调用add(double,int)方法,计算1.1+2:3.1
调用add(int)方法:1
调用add(double)方法:2.8
调用add(int...)方法:计算1加到9的和:45
抽象类:
抽象类通过关键字abstract定义,其语法如下:
public abstract class Test
{
abstract void fun();
}
使用abstract定义的类称为抽象类,用abstract定义的方法叫做抽象方法。抽象方法没有方法体,本身没有任何意义,只能通过子类的重写赋予其意义。只要类中有抽象方法,这个类就是抽象类。继承抽象类的子类需要将抽象类当中的抽象方法进行覆盖。
接口:
接口是纯粹的抽象类,接口中的所有方法都没有方法体,其内定义的任何字段都自动是static和final的。接口使用interface关键字进行定义,一个类实现一个接口则可以使用implements关键字实现。
还是通过四边形类来理解接口:
interface Fun //定义接口
{
public void fun(); //定义接口的方法
}
class Parallelogram extends Quadrangle implements Fun //平行四边形类,继承于四边形类,并实现Fun()接口
{
public void fun() //实现接口,覆盖接口类的方法
{
System.out.println("成功调用平行四边形类的fun()方法。");
}
public void dosomething() //重写,覆盖父类的方法
{
System.out.println("成功调用平行四边形类的dosomething()方法。");
}
}
class Square extends Quadrangle implements Fun //正方形类,继承于四边形类,并实现Fun()接口
{
public void fun() //实现接口,覆盖接口类的方法
{
System.out.println("成功调用正方形类的fun()方法。");
}
public void dosomething() //重写,覆盖父类的方法
{
System.out.println("成功调用正方形类的dosomething()方法。");
}
}
public class Quadrangle //四边形类
{
public void dosomething()
{
System.out.println("成功调用四边形类的dosomething()方法。");
}
public static void main(String args[])
{
Fun x[] = {new Square(),new Square(),new Parallelogram()}; //接口的向上转型
int i;
for(i=0;i<x.length;i++)
{
x[i].fun();
}
}
}
程序输出:
成功调用正方形类的fun()方法。
成功调用正方形类的fun()方法。
成功调用平行四边形类的fun()方法。
Java中不允许出现多重继承,但是可以利用接口实现“多重继承”,因为一个类可以同时实现多个接口,格式如下:
class 类名 implements 接口1,接口2......
————————————————————————————————————————————————————
类包:
Java中提供了一种管理类文件的机制——类包。类包中包含了一些类和接口,每一个类和接口也都必须隶属于一个类包。例如之前提到过的Math类,它的完整名称是java.lang.Math类,其中java.lang就是Math类所处的类包名称。运用类包,我们就可以在不同的类包当中定义同名称的类或接口而不发生编译错误。类包之间也可以相互访问,同一个类包之间访问可以不指定包名。对于没有定义在包内的类,系统会自动将其转移到默认包中。
指定包名通过package关键字实现,该表达式必须放置在程序的第一行。默认包中的package语句可以省略。
下面我们通过一个例子来更好地理解类包:
我们先打开项目的目录,其中的src目录下有一个叫default package的东西,这个便是默认包。我们可以再在src目录下新建一个包,我将其命名为my.package1。之后再在这个新的类包中新建一个类Math,这个Math类和java.lang.Math类是不一样的:
新建Math类之后发现在程序的第一行多了package语句,这条语句指明了这个类所处的类包名称:
package my.package1; //指定包名
public class Math
{
public static void main(String args[])
{
System.out.println("此Math类非java.lang.Math类!而是my.package1.Math类!");
}
}
程序输出:
此Math类非java.lang.Math类!而是my.package1.Math类!
我们再在这个新类包中写一个新类Physics:
package my.package1;
public class Physics
{
public static void fun()
{
System.out.println("成功调用my.package1.Physics类!");
}
public static void main(String args[])
{
}
}
我们再新建一个类包my.package2,在里面写一个Test类。
此时,如果想要在my.package2.Test类中使用my.package1中的Physics类,则需要使用import关键字进行导入:
package my.package2;
import my.package1.Physics;
public class Test
{
public static void main(String args[])
{
Physics.fun();
}
}
程序输出:
成功调用my.package1.Physics类!
当一次性需要调用一个类包中的多个类的时候,逐个类地进行import很是麻烦,这个时候我们可以用“ * ”来代替类名,从而直接导入整个类包的所有类。
import my.package1.*;
import关键字还可以用来导入静态成员:
package my.package2;
import static java.lang.Math.max; //导入静态成员方法
import static java.lang.System.out; //导入静态成员变量
public class ImportTest
{
public static void main(String args[])
{
out.println("1和2中的较大值为:"+max(1,2)); //直接使用静态成员
}
}
程序输出:
1和2中的较大值为:2
final:
final关键字用于修饰常量,被定义为final的常量在定义时需要使用大写字母命名,并且在中间使用下划线进行连接,使用final修饰的常量必须在声明的时候赋值,且这个常量在之后的运行过程中只能恒定地指向一个对象,无法将其改变以指向另外一个对象。可以根据下面这个程序来更好地理解用final定义常量的方法:
package my.package2;
import static java.lang.System.out;
import java.util.Random;
class A
{
int i=0;
}
public class FinalTest
{
static Random r=new Random();
private final int VALUE_1=9; //声明一个final常量
private static final int VALUE_2=10; //声明一个static final常量
private final A a1=new A(); //声明一个final引用
private A a2=new A(); //声明一个非final引用
private final int a[]= {1,2,3,4,5}; //声明一个final常数组
private final int r1=r.nextInt(10); //声明一个final随机数
private static final int r2=r.nextInt(10); //声明一个static final随机数
public String toString()
{
return (r1+" "+r2);
}
public static void main(String args[])
{
FinalTest t=new FinalTest();
//t.a1=new A(); 会报错
//t.VALUE_2++; 会报错
t.a2=new A();
int i;
for(i=0;i<t.a.length;i++)
{
//a[i]=1; 会报错
}
out.println(t);
out.println(new FinalTest());
out.println(t);
}
}
程序随机输出:
4 0
6 0
4 0
1 6
1 6
1 6
可以看出,将随机数用final修饰,每次运行时可以改变其值,但是用static final所修饰的值不变。
除了用final修饰常量之外,我们还可以用final修饰方法,这样就可以防止子类修改该类的定义与实现方式,即不能被子类覆盖,并且final可以提高方法的执行效率。用private修饰的方法无需再用final进行修饰,其已经被隐式地声明为final类型。
用final修饰的类称为final类,其无法被继承。如果希望一个类不改动、不继承,则可以将其声明为final类。在成为final类之后,该类中所有的方法都被隐式声明为final类型,但是该类中的变量却可以定义为非final类型。
内部类:
顾名思义,内部类就是在类中定义的类,内部类可以分为成员内部类、局部内部类、匿名内部类。
1.成员内部类
成员内部类可以直接访问这其所在类的私有成员。即内部类可以访问外部类,但是外部类无法访问内部类。内部类的对象会依赖于外部类的对象。非内部类不能被声明为private或protected类型。
可以通过下面这个程序加以理解:
public class OuterClass //外部类
{
innerClass in=new innerClass(); //在外部类实例化内部类对象引用
public void outf() //在外部类方法中调用内部类方法
{
in.inf();
}
class innerClass //内部类
{
innerClass() //内部类构造方法
{
System.out.println("成功调用内部类构造方法。");
}
public void inf() //内部类成员方法
{
System.out.println("成功调用内部类的inf()方法。");
}
int x=0; //内部类成员变量
}
private innerClass dosomething() //外部类成员方法
{
System.out.println("成功调用外部类的dosomething()方法。");
return new innerClass(); //返回值为内部类的引用
}
public static void main(String args[])
{
OuterClass out=new OuterClass();
//内部类的对象实例化操作必须在外部类或外部类的非静态方法中实现:
OuterClass.innerClass in1=out.dosomething();
OuterClass.innerClass in2=out.new innerClass();
}
}
程序输出:
成功调用内部类构造方法。
成功调用外部类的dosomething()方法。
成功调用内部类构造方法。
成功调用内部类构造方法。
当外部类的成员变量和内部类的成员变量的名称相同时,可以使用this关键字加以区分:
public class TheSameName
{
private int x;
private class Inner
{
private int x=1;
public void fun(int x)
{
x++; //调用fun()方法的形参x
this.x++; //调用内部类的成员变量x
TheSameName.this.x++; //调用外部类的成员变量x
}
}
}
2.局部内部类
内部类不光可以在外部类中定义,还可以通过接口从而在外部类的成员方法中定义成局部内部类:
interface OutInterface{} //定义接口
public class OuterClass //外部类
{
public OutInterface dosomething(final String x) //外部类的成员方法,参数为final型
{
class InnerClass implements OutInterface //局部内部类,实现接口
{
InnerClass(String s)
{
s=x;
System.out.println(s);
}
}
return new InnerClass("dosomething");
}
}
3.匿名内部类
匿名内部类很strange,其没有任何名称,也不需要用到class关键字进行定义,定义匿名内部类使用的是return语句。匿名内部类实际上就是创建一个实现于接口的匿名类的对象。
匿名内部类通过默认构造方法来实现接口的对象,在匿名内部类定义结束之后的大括号后面需要加上分号,这个分号并不是表示内部类的结束,而是代表接口引用表达式的创建。
对上面的局部内部类的示例进行稍微的改动即可实现匿名类:
interface OutInterface{} //定义接口
public class OuterClass //外部类
{
public OutInterface dosomething() //定义外部类成员方法
{
return new OutInterface() //声明匿名内部类
{
private int i=0;
public int getValue()
{
return i;
}
}; //要加分号!
}
}
那么问题来了:主方法可以写在内部类里面吗?
答案是肯定的,但前提是得把内部类声明成static型的。此外,被static声明的静态内部类不能调用外部类的非静态成员变量,这点需要注意:
public class StaticInnerClass
{
int x=100;
static int y=200;
static class Inner
{
void dosomething()
{
System.out.println("成功调用内部类的成员方法!");
}
public static void main(String args[])
{
//System.out.println(x); //静态内部类不能调用外部类的非静态成员变量
System.out.println(y);
}
}
}
程序输出:
200
最后我们来看看内部类的继承:
内部类当然可以被继承,只是过程稍微复杂一些,需要通过专门的语法来完成。我们创建一个外部类A,再创建一个外部类B,再在外部类B当中创建一个内部类C,则实现A继承于内部类C的语法如下:
public class A extends B.C //使A继承B类的内部类C
{
public A(B b)
{
b.super(); //必须硬性给予A类一个带参数的构造方法,这样才为继承提供了必要的对象引用
}
}
class B
{
class C
{
}
}
————————————————————————————————————————————————————
今天先学习Java中的异常处理机制。
异常在Java中是以类的实例的形式出现的,当某一个方法发生错误时,这个方法就会自动创建一个异常对象,从而使我们在编写代码的同时在其他地方处理异常。
Java的异常捕获结构由try、catch、finally三部分所组成。其中,try语句块存放的是可能发生异常的Java语句;catch程序块常与try搭配,其在try之后激发被捕获的异常;而finally语句块是异常处理结构的最后执行部分。
下面我们通过Take类来理解try-catch结构:
public class Take
{
public static void main(String args[])
{
try
{
String name="Jeron Zhou";
System.out.println(name+"的年龄是:");
int age=Integer.parseInt("19abcd"); //字符串中含有字母,不可以被转化为整型数字,故产生异常
System.out.println(age);
}
catch(Exception e)
{
e.printStackTrace(); //输出错误性质
}
System.out.println("程序执行完毕!");
}
}
程序输出:
Jeron Zhou的年龄是:
java.lang.NumberFormatException: For input string: "19abcd"
程序执行完毕!
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Take.main(Take.java:9)
可以看出,尽管产生了异常,但是程序并没有因为异常而直接退出,还是执行到了最后,即输出“程序执行完毕!”。
try代码块发生异常之后便会立即跳转到catch代码块,继续执行catch之后的语句,而不执行try代码块尚未执行的部分,直至程序结束。
至于catch语句中的Exception则是try代码块传递给catch代码块的变量类型,e是变量名,常用的获取异常信息的方法有三种,其作用如下所示:
e.getMessage(): 输出错误性质。
e.toString(): 给出异常的类型与性质。
e.printStackTrace(): 指出异常的类型、性质、栈层次以及位置。
由于printStackTrace()方法的作用最丰富,所以经常优先使用。
经过系统提示,发现是给的age字符串中出现了字母,所以无法正确将字符串转换成整型数字,经过修改之后,程序的异常得以处理:
Jeron Zhou的年龄是:19
程序执行完毕!
当然,完整的异常处理语句一定是要包含finally的,但是在这四种情况下finally语句不会被执行,这些情况不常见,了解即可:
1.在finally语句块内发生了异常。
2.在finally之前的代码中使用了System.exit()方法直接退出了程序。
3.程序所在的线程死亡。
4.CPU被关闭。
除了Java内置的异常类,我们还可以自己定义异常,只需将自定义异常继承异常类即可。使用自定义异常类的步骤如下:
1.创建自定义异常类。
2.在方法中通过throw关键字抛出异常对象。
3.如果在当前抛出异常的方法中处理异常,则可以使用try-catch语句块捕获并处理,否则就需要在方法的声明处通过throw关键字指明要抛出给方法调用者的异常,继续执行下一步操作。
4.在出现异常方法的调用者中捕获并处理异常。
文字表述过于抽象,还是用代码做示例更为直观:
class MyException extends Exception //创建自定义异常,继承Exception类
{
public MyException(String ErrorMessage) //构造方法,参数为输出的异常信息
{
super(ErrorMessage); //父类构造方法
}
}
public class Take2
{
static int f(int n1,int n2) throws MyException //定义方法f,计算0~100内任意两个整数的平均值,抛出异常给MyException
{
if(n1<0 || n2<0) //判断参数是否满足条件:出现负数的异常情况
{
throw new MyException("Error,不可以使用负数!"); //抛出异常给MyException,输出错误信息
}
if(n1>100 || n2>100) //判断参数是否满足条件:出现大于100的数的情况
{
throw new MyException("Error,数值超过100!"); //抛出异常给MyException,输出错误信息
}
return (n1+n2)/2;
}
public static void main(String args[]) //主方法
{
try //处理可能异常的代码
{
int x=f(-1,-2); //出现负数的异常情况
int y=f(150,55); //出现大于100的数的异常情况
System.out.println(x);
System.out.println(y);
}
catch(MyException e) //输出异常信息
{
System.out.println(e);
}
}
}
程序第一次输出:
MyException: Error,不可以使用负数!
按照提示修改参数之后,程序第二次运行输出:
MyException: Error,数值超过100!
再次按照提示修改参数之后,程序第三次运行输出方才正常。
从这个程序可以看出,throws关键字通常被应用在声明方法时,其用来指定方法可能抛出的异常,由于方法内可能存在不止一种异常,故为throws。使用throws关键字将异常抛给上一级后,如果不想处理该异常,则可以继续向上抛出,但最终要有能处理这个异常的代码。
而throw关键字通常被用于方法体中,用来抛出一个异常对象。程序在执行到throw语句时便立即终止,后面的语句则不予运行。捕捉throw抛出的异常必须使用try-catch结构。
Java类库的每个包都有自己的异常类,它们都是Throwable类的子类,Java的异常类结构如下:
其中的RuntimeException异常类最为常见,其中的异常种类详见下表:
学完异常处理后我突然发现了一个问题:学了十几天的Java,写的程序居然都没有输入,只有输出!(是说怎么感觉怪怪的)
所以,接下来我们来看看I/O流,也就是输入输出流:
同C++当中的cin和cout类似,Java中也由数据流处理输入输出模式,输入流中可以是数据、文件、网络、压缩包等等,而输出流的目标可以是文件、网络、压缩包、控制台或其他目标。可通过下图来直观了解I/O流的运作过程:
Java中有各种的关于输入输出的类,它们都被包含在包java,io中。其中所有的输入流类都是抽象类字节输入流InputStream或字符输入流Reader的子类;而所有的输出流类都是抽象类字节输出流OutputStream或字符输出流Writer的子类。以上所涉及的父类以及它们的子类的层次结构如下图所示:
Reader及其子类:
OutputStream及其子类:
Writer及其子类:
由于所涉及的输入输出方法过多,在此就不逐一解释了。当然,其实也没有必要每一种都记忆,学会使用几种常用的方法即可。
具体的使用功能请详见下面这篇博文:
Java中输入输出流详解
值得一提的是,I/O流多用于处理文件、网络、压缩包的输入输出,而平常的字符输入很少用java.io实现,通常直接使用System.in中的Scanner类实现。下面我们来看一个例子:
import java.util.*; //导入包
public class Input
{
public static void main(String args[])
{
Scanner s=new Scanner(System.in); //创建对象
System.out.print("请输入您的姓名:");
String name=s.nextLine(); //等待输入字符串数据
System.out.print("请输入您的年龄:");
int age=s.nextInt(); //等待输入整型数据
System.out.print("请输入您的身高(m):");
double height=s.nextDouble(); //等待输入双精度数据
System.out.println();
System.out.println("姓名:"+name+" 年龄:"+age+" 身高(m):"+height);
s.close(); //若没有关闭Scanner对象将会出现警告
}
}
程序输入:
张三
25
1.78
程序输出:
姓名:张三 年龄:25 身高(m):1.78
在实现I/O流对文件进行输入输出前,我们先得了解一下文件类File。
File类是java.io包中唯一代表磁盘文件本身的对象,其中的方法可以实现创建、删除、重命名文件等操作。File类有三种构造方法:
1)File file=new File(String pathname)
其中pathname指的是文件的路径名称。
File file=new File("D:/Java/123.java");
2)File file=new File(String parent,String child)
其中parent指的是父路径字符串,child指的是子路径字符串。
File file=new File("D:/Java","123.java");
3)File file=new File(File f,String child)
其中f代表了父路径的对象,child代表子路径字符串。
File file=new File(D:/Java/,"123.java");
如果所调用的目录中没有目标文件,则File类会调用creatNewFile()方法创建一个同名的空文件。
下面用这段代码理解文件的创建过程:(这个文件在路径中并不存在)
import java.io.File; //导入文件类
public class FileTest
{
public static void main(String args[])
{
File file=new File("word.txt"); //创建文件对象
if(file.exists()) //如果这个文件已经存在
{
file.delete(); //则将文件删除
System.out.println("文件已成功删除。");
}
else //如果文件不存在
{
try //捕捉可能出现异常,可省略
{
file.createNewFile(); //则创建该文件
System.out.println("文件已成功创建。");
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
}
程序输出:
文件已成功创建。
File类中提供了多种方法以获取文件的本身信息,将上面的代码稍加修改即可得到如下程序:
import java.io.File;
public class FileTest
{
public static void main(String args[])
{
File file=new File("word.txt");
if(file.exists())
{
String name=file.getName(); //获取文件名称
long length=file.length(); //获取文件长度
boolean hidden=file.isHidden(); //判断文件是否隐藏
System.out.println("文件名称:"+name);
System.out.println("文件长度:"+length);
System.out.println("文件是否隐藏? "+hidden);
}
else //文件不存在的情况
{
System.out.println("该文件不存在!");
}
}
}
程序输出:
文件名称:word.txt
文件长度:0
文件是否隐藏? false
在初步了解了文件类File之后,我们就可以了解FileInputStream类和FileOutputStream类了。这两个类都继承自InputStream类,二者分别对应着文件I/O流。
FileInputStream类的构造方法有两种形式:
FileInputStream(String name)
FileInputStream(File file)
其中第一种构造方法比较简单,其使用给定的文件名name创建一个FileInputStream对象;第二种构造方法是用File对象创建FileInputStream对象,这样做的优点是允许在把文件连接输入流之前对文件作进一步的分析。
还是利用word.txt这个文件作为示例(感谢word.txt文件!):
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class File_IO_Test
{
public static void main(String args[])
{
File file=new File("word.txt"); //创建文件类对象
/*————文件流输入————*/
try
{
FileOutputStream out=new FileOutputStream(file); //创建FileOutputStream对象,实现文件流输入
byte x[]="输入输出流让我想睡觉".getBytes(); //创建byte型数组x,用于存放文件信息
out.write(x); //将数组中的信息写入文件中
out.close(); //将文件输入流关闭
}
catch(Exception e)
{
e.printStackTrace();
}
/*————文件流输出————*/
try
{
FileInputStream in=new FileInputStream(file); //创建FileInputStream对象,实现文件流输出
byte y[]=new byte[1024]; //创建byte型数组y,用于存放文件信息
int length=in.read(y); //从文件中读取信息
System.out.println("word.txt文件中的信息是:"+new String(y,0,length)); //将文件中信息进行流输出
in.close(); //将文件输出流关闭
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
程序输出:
word.txt文件中的信息是:输入输出流让我想睡觉
可以看到,在使用完输入输出流之后要对流进行close()操作,即把流关闭。虽然Java程序结束的时候会自动关闭所有的流,但因为打开的流可能会占用系统资源,所以还是显式地关闭流比较规范。
java.io包中还有带缓存的输入输出流、数据输入输出流、压缩包输入输出流等。知识繁多,显然一天内无法完全掌握,以后碰到了再进行补充吧。
更多关于 I/O流的知识可移步至这些博客(感谢大神的总结!):
详解Java中的I/O流(这篇讲的很详细,且通俗易懂,强烈推荐!)
带缓存的I/O流
数据的I/O流和zip压缩包的I/O流
————————————————————————————————————————————————————
今天学习Java中有关线程的知识。
多个程序同时进行,这种情况称为并发,而将并发完成的每一件事请都统称为线程。在程序中执行多个线程,每个线程完成特定的功能,这种机制就是多线程。
Java中通过两种方式实现线程,第一种是继承java.lang.Thread类,第二种是实现java.lang.Runnable接口。
1.继承Thread类实现线程
Thread类的实例化对象代表线程,其有两种构造方法:public Thread()和public Thread(String threadName)。完成线程功能的代码放到run()方法中,在继承Thread类之后就可以对run()方法进行覆盖,覆盖之后调用start()方法就可以开始执行线程了,如果start()方法调用了一个已经启动的线程,则会抛出异常。
我们通过输出1~10的程序来理解线程:
public class ThreadTest extends Thread //继承Thread类
{
private int n=1;
public void run() //重写run()方法
{
while(true)
{
System.out.print(n+" ");
if(n++==10)
{
return ;
}
}
}
public static void main(String args[])
{
new ThreadTest().start();
}
}
程序输出:
1 2 3 4 5 6 7 8 9 10
从这个程序中可以看出,我们可以在run()方法中使用无限循环,从而让线程得以一直运行下去,所以一定要设置跳出循环的条件。在main方法中要让线程执行需要调用start()方法,否则线程永远不会启动。没有启动的线程就不能称之为线程了,仅仅是Thread类的一个实例而已。
2.实现Runnable接口实现线程
实现Runnable接口的语法如下:
public class Thread extends Object implements Runnable
实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象与Thread对象相关联。此时,Thread类有public Thread(Runnable target)和public Thread(Runnable target,String name)两种构造方法。
使用Runnable接口启动新的线程的步骤为:
1.建立Runnable对象。
2.使用参数为Runnable对象的构造方法创建Thread实例。
3.调用start()方法启动线程。
线程的生命周期包含了7种状态:出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态、死亡状态。比如,一个线程在用start()方法调用之前都是出生状态;调用start()方法后就处于就绪状态;线程得到系统资源后就进入运行状态…
只要线程进入了就绪状态,那么这个线程将会在就绪和运行状态之间来回转换,或者进入等待、休眠、阻塞等状态。
当处于运行状态下的线程调用wait()方法时,线程便进入等待状态,再次唤醒则要使用notify()方法。
当处于运行状态下的线程调用sleep()方法时,线程便进入休眠状态。
当处于运行状态下的线程有输入或输出请求时,线程便进入阻塞状态,输入输出结束时又返回就绪状态。
当线程的run()方法执行完毕的时候,该线程便进入了死亡状态。
使线程处于就绪状态的方法有:
1.调用sleep()方法。
2.调用wait()方法。
3.等待输入输出完成。
处于就绪状态后,让线程再次回到运行状态的方法有:
1.调用notify()方法。
2.调用notifyAll()方法。
3.调用interrupt()方法。
4.线程的休眠时间结束。
5.输入输出结束。
sleep()方法:
try
{
Thread.sleep(1000); //让线程休眠1秒
}
catch(Exception e)
{
e.printStackTrace();
}
sleep()方法的参数的单位是毫秒,1000毫秒即是一秒,所以上述代码的功能是让线程休眠1秒,1秒后进入就绪状态。
join()方法:
join()方法用于将一个线程加入到另外一个线程中,被加入的线程需要等加入的线程先执行完毕之后再执行原本的线程。
线程的优先级:
每个线程都有其各自的优先级,通过优先级可以获悉一个线程在程序中的重要性,优先级高的先执行,优先级低的后执行。
线程的优先级分为1~10十个等级,最低优先级为Thread.MIN_PRIORITY,用数字1表示;最高的优先级为为Thread.MAX_PRIORITY,用数字10表示;一个线程默认的优先级是Thread.NORM_PRIORITY,用数字5表示。线程的优先级可以用setPriority()方法在1 ~10之间进行调整。
说到多线程,多线程看起来是多个线程同时执行,其实不然。在多线程情况下,每个线程都会得到一段CPU时间片,在这个时间结束时,就会轮换到另一个线程执行,由于切换较快,所以会给人一种同时进行的假象。
一个线程结束之后,系统就会选择与当前线程优先级相同的线程予以运行,当同一优先级的所有线程执行完之后才考虑执行次级优先级的线程。
线程同步:
多线程程序可能会发生两个线程抢占资源的问题,为了防止这些冲突,我们可以利用线程同步机制来解决。
我们通过模拟火车票售票系统的功能来理解线程同步:
public class TrainTickets implements Runnable //实现接口Runnable
{
int num=10; //总票数num为10张
public void run() //执行线程
{
while(true)
{
if(num>0)
{
try
{
Thread.sleep(100);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println("tickets "+num--); //从tickets10出票到ticket1,输出出票
}
}
}
public static void main(String args[])
{
TrainTickets t=new TrainTickets(); //实例化对象
//以该类对象分别实例化4个线程
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
Thread t4=new Thread(t);
//分别启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
程序输出:
tickets 10
tickets 9
tickets 7
tickets 8
tickets 6
tickets 5
tickets 4
tickets 5
tickets 3
tickets 2
tickets 1
tickets 0
tickets -1
在没有运用线程同步的情况下,发现输出了tickets 0和tickets -1,这显然与事实相悖,毕竟票只有10张,理应输出tickets 1就结束了。其原因就是:当两个线程同时访问代码块时,第一个线程成功输出结果,但此时第二个线程已经执行完判断却没有执行输出,这个时候第二个线程来不及再次判断票数是否大于0,而是继续输出0以及后面的负数,从而造成了错误。
所以在处理多线程问题的时候,我们应该利用线程同步机制来消除这种可能的错误。修改后的代码如下:
public class TrainTickets implements Runnable //实现接口Runnable
{
int num=10; //总票数num为10张
public void run() //执行线程
{
while(true)
{
synchronized("") //同步块
{
if(num>0)
{
try
{
Thread.sleep(100);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println("tickets "+num--); //从tickets10出票到ticket1,输出出票
}
}
}
}
public static void main(String args[])
{
TrainTickets t=new TrainTickets(); //实例化对象
//以该类对象分别实例化4个线程
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
Thread t4=new Thread(t);
//分别启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
程序输出:
tickets 10
tickets 9
tickets 8
tickets 7
tickets 6
tickets 5
tickets 4
tickets 3
tickets 2
tickets 1
可以看到,在run()方法的循环体中加入了synchronized关键字之后,输出就变正常了。synchronized关键字用于声明同步块,在同步块中的线程可以避免资源冲突。
除了用synchronized关键字声明同步块实现线程同步外,我们还可以使用在方法前面加synchronized关键字进行修饰,从而让这个方法变成同步方法,该方法内的线程也不会产生资源冲突。改进后的代码块如下:
public synchronized void f() //定义同步方法
{
if(num>0)
{
try
{
Thread.sleep(100);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println("tickets "+num--); //从tickets10出票到ticket1,输出出票
}
}
public void run()
{
while(true)
{
f();
}
}
多线程编程与我熟悉的结构化编程、面向对象编程、泛型编程不同,它可以说是一种全新的编程思想(仿佛发现了新世界)。完全适应多线程编程的思维模式还是需要时间的。
由于多线程的机制很复杂,所以今天所学只能算是皮毛,只做大概了解。以后有时间的话应该要更深入的学习多线程编程的相关知识和思想。
————————————————————————————————————————————————————
自学Java已经两周了,今天来整点高级东西——Java中的数据库操作。
数据库:
在学习Java的数据库操作之前,先得了解一下什么叫数据库。数据库是一种存储结构,它允许使用各种格式输入、处理和检索数据,反正很抽象就对了,可以理解为存放数据的仓库,可以做到随取随用。
数据库是从文件管理系统发展而来的,数据库则是数据管理的高级阶段。其有三个层次,由内而外分别是物理数据层、概念数据层、逻辑数据层。物理数据层是物理存储设备上实际存储的数据集合,概念数据层是数据库的整体逻辑表示的集合,逻辑数据层是用户看到和使用的数据库的数据集合。
数据库系统按数据模型不同,可分为多个种类:层次型数据库(树结构模型)、网状型数据库(图结构模型)、面向对象型数据库、关系型数据库(关系模型)等等。
SQL语言:
SQL意为结构化查询语言,其可以方便地查询、操作、定义和控制数据库中的数据。它由数据定义语言、数据操纵语言、数据控制语言、事务控制语言四大部分组成,其中我们使用的最多的也是最核心的部分就是数据操纵语言,它也和我们编写的应用程序息息相关。
下面列举SQL数据操纵语言中的几个常用语句及其用法:
1.select语句:用于从数据表中检索数据。语法如下:
select 所选字段列表 from 数据表名 where 条件表达式
group by 字段名 having 条件表达式(指定分组的条件)order by 字段名;
例如,在名为Student的数据表中检索出所有男性(sex-男)学生的姓名(name)、成绩(grade),并按成绩升序排序,它的SQL语句为:
select name,grade from Student where sex='男' order by grade;
2.insert语句:用于向数据表中插入新数据。语法如下:
insert into 表名[(字段名1,,字段名2...)] values(属性值1,属性值2...);
例如,在之前的Student数据表中插入一个数据(包含学号id、姓名name、性别sex、班级class、成绩grade这些属性),它的SQL语句为:
insert into Student values(2019308250301,'张三','男','计算机193',4.00);
3.update语句:用于更新数据表中的某些记录。语法如下:
update 数据表名 set 字段名=新的字段值 where 条件表达式;
例如,在插入张三之后发现他的成绩有误,我们需要修改一下,则它的SQL语句为:
update Student set grade=1.00 where id=2019308250301;
4.delete语句:用于删除数据。语法如下:
delete from 数据表名 where 条件表达式;
例如,因成绩过差,学校准备将张三劝退,这个时候我们就要把与他相关的数据删除,则它的SQL语句为:
delete from Student where id=2019308250301;
以上所述的四个语句均是SQL中的最基本语句。可以看出,SQL语言类似于简化版的英文,其语言逻辑与使用方法和自然语言比较相似,因此理解起来不是很困难。
更多SQL语句,请详见此篇博文:
SQL语句大全
JDBC:
那么,如何实现在Java中操作数据库呢?此时就需要用到JDBC技术,JDBC是一种执行SQL语句的Java应用程序设计接口(API),它是连接数据库和Java应用程序的纽带。由于SQL语言是面向关系的语言,依赖于关系模型,所以Java应用程序通过JDBC技术访问数据库也是面向关系的。
JDBC主要完成以下几个任务:
1.与数据库建立一个连接。
2.向数据库发送SQL语句。
3.处理从数据库返回的结果。
JDBC并不能直接访问数据库,JDBC必须依赖JDBC驱动程序才能访问数据库。JDBC驱动基本可分为这四种:
1.JDBC-ODBC桥:依靠ODBC驱动器和数据库通信。
2.本地API的一部分用Java编写的驱动程序。
3.JDBC网络驱动:最为灵活的驱动程序。
4.本地协议驱动:纯Java驱动的程序。
使用JDBC访问数据库首选JDBC网络驱动和本地协议驱动,因为这两种方法可以将Java的优点最大限度地发挥出来。
接下来学习一下JDBC中常用的类和接口:
1.Connection接口:
Connection接口代表与特定的数据库的连接,在连接上下文中执行SQL语句并返回结果。常用方法如下:
JDBC中有三种Statement对象:Statement、PreparedStatement、CallableStatement。其中Statement对象用于执行不带参数的简单SQL语句;PreparedStatement继承了Statement,用来执行动态的SQL语句;CallableStatement继承了PreparedStatement,用来执行对数据库存储的调用。
2.Statement接口:
Statement接口用于在已建立连接的基础上向数据库发送SQL语句。常用方法如下:
3.PreparedStatement接口:
PreparedStatement接口用于动态地执行SQL语句。常用方法如下:
ResultSet接口类似于一个临时表,其用来暂存数据库查询操作所获得的数据集。它的实例具有指向当前数据行的指针,指针开始的位置在第一条记录的前面,通过next()方法可将指针下移。常用方法如下:
5.DriverManner类:
DriverManner类用来管理数据库中的所有驱动程序,属于JDBC的管理层,其作用于用户和驱动程序之间,跟踪可用的驱动程序,并在数据库的驱动程序之间建立连接。类中的常用方法如下:
很显然,JDBC中常用的类和接口实在是太多了!
对于像我这样的初学者来说,每个方法都准确记忆并运用实在是难上加难。故现在只能将常用方法贴在这里,以便以后需要用的时候翻阅查看。
文字描述过于抽象,还是用程序来加深对Java中的数据库操作的理解:
访问数据库的第一步便是加载数据库的驱动程序,然后每次访问数据时创建一个Connection对象,接着执行操作数据库的SQL语句,最后在完成数据库操作后销毁前面创建的Connection对象,释放与数据库的连接。
import java.sql.*; //导入java.sql包
public class Database1
{
Connection x; //声明Connection对象
public Connection getConnection()
{
/*通过forName()来加载JDBC驱动程序,若失败则抛出ClassNotFoundException异常*/
try
{
Class.forName("com.mysql.jdbc.Driver");
System.out.println("数据库驱动加载成功!");
}
catch(ClassNotFoundException e)
{
e.printStackTrace();
}
/*通过getConnection()来建立数据库连接,若失败则抛出SQLException异常*/
try
{
final String url="jdbc:mysql://IP:3306/test"; //IP指的是IP地址
final String user="root";
final String password="123456";
x=DriverManager.getConnection(url,user,password); //参数分别为数据库的URL路径、用户名、密码
System.out.println("数据库连接成功!");
}
catch(SQLException e)
{
e.printStackTrace();
}
return x; //返回Connection对象
}
public static void main(String args[])
{
Database1 d=new Database1(); //创建本类对象
d.getConnection(); //调用连接数据库的方法
}
}
在第一次编译的时候,控制台报了很多错,经过查询,发现是因为没有导入相应的jar包。于是,我在项目当中新建了一个文件夹,命名为lib,并且把相应的jar包拖放到lib文件夹目录下,心想这回应该没问题了吧。
但是控制台还是报错。(又要debug了。。。)
查明原因之后,我右键单击项目,找到构建路径-配置构建路径,进入配置界面后,找到“库”对应的栏,在类路径中添加需要的jar包(已经在lib里面,很好找到)。
令人欣慰的是,控制台成功输出了“数据库驱动加载成功”。但还是报错:、
突然发现我的电脑里面还没有装MySQL数据库,于是我又装了MySQL。第三次运行的时候终于不报错了:
数据库驱动加载成功!
数据库连接成功!
这个程序成功实现了Java程序同MySQL数据库的连接。但是仅仅有连接是不够的,要执行SQL语句首先得获得Statement类的对象,方可向数据库发送SQL语句:
try
{
Statement sql=x.createStatement();
}
catch(SQLException e)
{
e.printStackTrace();
}
有了Statement对象之后,便可调用相应的方法实现对数据库的查询(顺序查询、模糊查询等)和修改,并将查询的结果集存放在ResultSet类的对象中。
ResultSet r=sql.executeQuery("select... from ...");
Java中的数据库操作实在是太繁杂了,没有办法做到面面俱到,所以我现在只能点到即止。又由于目前只是课前预习,我对MySQL的运作机制还不是很熟悉,再加上缺少相应的数据库,导致我没有办法写程序实例,这也加大了继续向前推进的难度。所以我决定暂缓学习关于数据库操作的部分,等到正式上课需要用到数据库的时候再进行补充也为时不晚。
更多关于Java中数据库的操作,可参考这篇博文:
Java数据库操作
————————————————————————————————————————————————————
欲知后事如何,请点击下方链接:
【Java】学习笔记3——从入门到菜鸡(Web编程&JSP篇)