一、数学
(一)几何
两圆相交(重叠)的条件:
圆心距小于两圆半径的和,而大于两圆半径的差的绝对值。
考虑两圆相交两点中的一点P,与两圆心成三角形:由”两边之和大于第三边“可得。
已知三角形三点,求三角形面积
S=| \frac{x_1(y_2-y_3)+x_2(y_3-y_1)+x_3(y_1-y_2)}{2} |
由向量的叉积公式得来,其中 $sin(\theta)$由点积中的$cos(\theta)$转换而来
已知三角形三边长,求三角形面积
S = \sqrt{\lambda \cdot (\lambda - e_1)(\lambda-e_2)(\lambda - e_3)}
以上为海伦公式。其中,$\lambda=\frac{e_1+e_2+e_3}{2}$
已知两点坐标,得出线段所在直线方程的一般式
设$p_1=(x_1,y_1),p_2=(x_2,y_2)$
两点式:$\frac{y-y_1}{x-x_1} = \frac{y_2-y_1}{x_2-x_1}$
一般式:$x(y_2-y_1) - y(x_2-x_1)+y_1(x_2-x_1)-x_1(y_2-y_1)=0$
即对于$ Ax+By+C=0$,有$A=(y_2-y_1), B=(x_1-x_2),C=y_1(x_2-x_1)-x_1(y_2-y_1)$
注意两点式要考虑$x_2=x_1 或y_1=y_2$的边界情况,但一般式的各系数表达式不用考虑这一边界(尽管是由两点式而来)
//由两点得出直线方程的一般式Ax+By+C=0
public double[] getEquation(){
double[] coefficient = new double[3];
coefficient[0] = p2.getY()-p1.getY(); //A
coefficient[1] = p1.getX()-p2.getX(); //B
coefficient[2] = p1.getY()*(p2.getX()-p1.getX()) - p1.getX()*(p2.getY()-p1.getY()); //C
return coefficient;
}
如何判断一点在线的哪一侧?
向量 $v{12}\times v{13}$若为正,说明点$p3$在线段向量$v{12}$的左边,为负则在右,为$0$表示向量平行,点$p_3$在线段或其延长线上
//判断点与线段向量的位置:返回0表示在线段上,返回+1表示在向量左侧,返回-1表示在向量右侧,返回2表示在延长线上
public int onWhichSide(MyPoint p3){
double[] vector12 = {p2.getX()-p1.getX(), p2.getY()-p1.getY()};
double[] vector13 = {p3.getX()-p1.getX(), p3.getY()-p1.getY()};
double cross12To13 = vector12[0]*vector13[1] - vector13[0]*vector12[1];
if(cross12To13 == 0){
if( ( p3.getX() > p1.getX() && p3.getX() < p2.getX() ) ||
p3.getX() > p2.getX() && p3.getX() < p1.getX() )
return 0;
else
return 2;
}
else if( cross12To13 > 0)
return 1;
else return -1;
}
已知端点坐标,如何判断两线段相交
方法一:
- 判断线段是否平行?若平行是否有重合?
- 不平行,解出直线的交点
若两条直线给出的一般式方程,如:
$$
\begin{equation}
\left{
\begin{aligned}
A_1x+B_1y+C_1=0 \
A_2x+B_2y+C_2=0\
\end{aligned}
\right.
\end{equation}
$$
则由克拉默法则可得$x= \left |\begin{array}{}
(-C_1) &B_1 \
(-C_2) &B_2 \
\end{array}\right| / ParallelFactor$,$y=\left |\begin{array}{}
A_1 &(-C_1) \
A_2 &(-C_2) \
\end{array}\right|/ ParallelFactor$,
其中若$ ParallelFactor =\left |\begin{array}{}
A_1 &B_1 \
A_2 &B_2 \
\end{array}\right| $为0,则表示两直线平行,方程无解。 - 判断该交点是否都在两条线段上
public boolean intersection(LineSegment2D line2){
double[] coLine1 = this.getEquation();
double[] coLine2 = line2.getEquation();
double parallelFactor = getDet2D(coLine1[0],coLine1[1],coLine2[0],coLine2[1] );
//判断线段是否平行?若平行是否有重合?
if( parallelFactor == 0){
if (this.onWhichSide(line2.p1)==0 || this.onWhichSide(line2.p2)==0)
return true;
else return false;
}
//不平行,解出直线的交点
else{
double cpX = getDet2D(-coLine1[2],coLine1[1],-coLine2[2],coLine2[1] )/parallelFactor;
double cpY = getDet2D(coLine1[0],-coLine1[2],coLine2[0],-coLine2[2] )/parallelFactor;
MyPoint crossPoint = new MyPoint(cpX,cpY);
//判断交点是否都在两条线段上
if( this.inTheRecZoneOfLineSegment(crossPoint) && line2.inTheRecZoneOfLineSegment(crossPoint))
return true;
else return false;
}
}
//其中 inTheRecZoneOfLineSegment 方法用于判断交点是否在两条线段上
已知顶点坐标,如何判断点是否在三角形中?
- 面积相加是否相等
- 该点与三点所成的三个夹角之和是否为360°
- 该点与三点连线所在的三条直线是否和三点的对边相交
- 用凸包
已知顶点坐标,如何判断两个三角形是否相交?
已知顶点坐标,如何判断两个矩形是否相交?
方法一:至少一个点在对方内部
方法二:取每个矩形的对角线的两个顶点(如左上顶点 left-top
和右下顶点 right-bottom
)
//判断两矩形是否重叠,若重叠,则至少一个点在彼此内部(contains方法考虑了点在矩形边上的情况)
public boolean overlapsByContain(Rectangle2D rec2){
return( this.contains(rec2.left_top) || this.contains(rec2.right_top) ||
this.contains(rec2.left_bottom) || this.contains(rec2.right_bottom));
}
//判断两矩形是否重叠,若重叠,则一个矩形在另一个的左边往左或者上边往上
public boolean overlapsByVertex(Rectangle2D rec2){
if ( this.right_bottom.getX() < rec2.left_top.getX() || rec2.right_bottom.getX() < this.left_top.getX())
return false;
if ( this.left_top.getY() < rec2.right_bottom.getY() || rec2.left_top.getY() < this.right_bottom.getY())
return false;
return true;
}
求凸多边形面积
已知各点坐标,顺时针排列为:$(x_1,y_1),(x_2,y_2)…(x_n,y_n)$
Area=\frac{1}{2}\left |\begin{array}{}
x_1 & y_1 \
x_2 & y_2 \
x_3 & y_3 \
.. & .. \
x_n & y_n
\end{array}\right|=\frac{1}{2}{(x_1y_2+x_2y_3+…+x_ny_1)-(y_1x_2+y_2x_3+…+y_nx_1)}
(二)代数
判断是否是素数
- 如果在2到根号N之内(闭区间)都没有数整除N,则N是素数。
- 对于
BigInteger
,采用实例方法bigNum.isProbablePrime(int certatinty)
certainty
参数可以以 $1 - 1/2^{certatinty}$的可能性返回结果
例子见习题10.18
素数分解
public static ArrayList<Integer> primeFactorization(long num){
ArrayList<Integer> list = new ArrayList<>();
//step1: divide num by 2 till num can't be divided by 2
while( num % 2 == 0){
list.add(2);
num /= 2;
}
//step2: i start at 3, increment by 2 when i < sqr(num) ;
// divide num by every i till num can't be divided by i
for(int i = 3; i < Math.sqrt(num); i+=2){
while ( num % i == 0){
list.add(i);
num /= i;
}
}
//The above two steps is to find prime factors more quickly
//step3: corner-case: now num itself is a prime number BUT it's greater than 2
if( num > 2 )
list.add((int)num);
return list;
}
寻找完全平方数
对于整数m,找到整数n使得 m*n是某个数的完全平方
把m解析为最小因数的乘积,出现了奇数次的最小因数各相乘一次,就是n
例如,m= 90 =2 3 3 5, n = 2 5 = 10,m * n = 900 是完全平方数。
求最大公约数
public static int greatestCommonDivisor( int n1,int n2) {
int gcd = 0;
int test = 1;
while (test <= n1 && test <= n2) {
if ( n1%test==0 && n2%test==0)
gcd=test;
test++;
}
return gcd;
}
//不能把循环条件设置成test <= n1/2 && test <= n2/2,这样当n1或n2就是最大公约数时则被忽略了,例如n1=144,n2=48,这种方法求出来的最大公约数是24而不是48
判断是否回文数
public static boolean isPalindrome ( int number ) {
String numString = ""+number;
for ( int i = 0 ; i < (numString.length()/2) ; i++) {
if ( numString.charAt(i) != numString.charAt( numString.length() - i -1 ) ) {
System.out.println("第 "+(i+1)+" 位与第 "+(numString.length() - i )+" 位不同。 ");
return false; //只要发现由不同的位,用return 返回false 并直接跳出方法
}
else
System.out.println("第 "+(i+1)+" 位与第 "+(numString.length() - i )+" 位相同,继续判断");
}
return true;
}
模运算
先除1000再模60;与直接模60000是不一样的
long totalSeconds = System.currentTimeMillis() / 1000;
long totalMinutes = totalSeconds % 60;
long wrongTotalMinutes = System.currentTimeMillis() % 60000
浮点数的表示精度
为什么10.03的double在计算时会变成10.029999999,而10.25可以被精确表达?
这是因为IEEE 754标准下的浮点数使用二进制计数,即0.25=1×2;但0.03则不能用二进制计数法表达。因此754标准所说的表示范围不是一个连续实数集,中间有很多小数无法被精确表达。
如何避免精度损失?
将小数位提到整数部分计算;采用bigDecimal类;
产生随机数
- 使用
int random = System.out.currentTimeMillis() % 10;
产生[0,9]
区间的整数; 使用
double random = Math.random();
产生[0.0,1.0)
区间的浮点数;double k = 10 + Math.random()*10; //产生[10,20)的随机浮点数;
int m = 10 + (int)(Math.random()*21); // 产生[10,20]的整数
能否用此方法产生
[10,20
]的浮点数?(10,20]
的浮点数或整数?- 使用
Random类
;待补充 ```java import java.util.Random; Random random = new Random();
int k = random.nextInt(6) ; // 产生 [0.6)的整数
<a name="9a37f36d"></a>
#### 进制转换
给定某数值型字符串,返回给定进制数
<a name="2e3fe4d9"></a>
##### 十进制 到 其他进制
- 用静态方法`String.format`
```java
return String.format("%x",26);
//-> 1A (字符串)
- 自定义
其他进制 到 十进制
用包装对象的静态方法
Integer.parseInt
或Integer.parseDouble
return Integer.parseInt("1A",16);
//-> 26
自定义
//args:String s
//uppercase
String hexString = s.toUpperCase();
//compute
long decResult = 0;
for(int i = 0; i < hexString.length(); i++){
char thisChar = hexString.charAt(i);
if( thisChar>='0'&& thisChar <='9'){
decResult = decResult*16 + (thisChar-'0') ;
}
else
decResult = decResult*16 + (thisChar - 'A' + 10);
}
return decResult;
字符串
Java允许没有引用变量直接使用字符串字面值来调用该实例对象的方法,那么这种没有引用变量的实例对象是如何保存、以及内存回收的呢?
break语句
随机产生0/1值
int i = (int)Math.rint(Math.random());
计算某天星期几
- 泽勒一致性算法:输入年份、月份、某月的第几天;得到该天是星期几。P97
计算多边形面积
已知各点坐标,计算各个三角形面积再相加:
s = (e1+e2+e3)/2;
area = Math.sqt( s*(s-e1)*(s-e2)*(s-e3) );
正n边形且已知外接圆的半径r:
area = (r*r*n)/( 4*Math.tan(Math.PI/n) );
计算球面上两点距离(最大圆距)
d = 半径 * Math.acos( Math.sin(x1)*Math.sin(x2)+Math.cos(x1)*Math.cos(x2)*Math.cos(y1-y2) );
输入一个字符
一般方法
char c = reader.next().charAt(0);
char c = reader.getLine().charAt(0);
//reader是一个Scanner对象实例
To consume exactly one character you could use:
char c = reader.findInLine(".").charAt(0);
To consume strictly one character you could use:
char c = reader.next(".").charAt(0);
常用字符的ASCII值
'a'=97; 'A'=65; '0'=48; '9'=57;
十进制转十六进制
- 自定义方法一
public static String decTohex(int dec) {
char hexChar[] = {'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F'};
String hex = "";
if (dec==0)
hex = "0";
while ( dec != 0 ) {
hex += hexChar[ dec%16 ];
dec /= 16;
}
return hex;
}
自定义方法二
public static String decToHex(int dec) {
String hex = "";
int hexDigitValue = -1;
if( dec==0 )
hex = "0";
while(dec!=0) {
hexDigitValue = dec % 16;
if (hexDigitValue >= 0 && hexDigitValue <=9)
hex = (char)(hexDigitValue+'0')+hex;
else
hex = (char)(hexDigitValue-10+'A')+hex;
dec /= 16;
}
return hex;
library method
String hex = Integer.toHexString(dec);
字符与数值类型之间的转换
- 字符=>数字
字符的Unicode在一个字节范围内,自动隐式转换;超过一个字节须强制显示转换。 - 数字=>字符
整型值只能转换其低16位的值对应的Unicode字符;浮点值须先(内部)转为整型再转为字符。
十六进制(字符)转换为十进制
public static int hexChar2Dec(char hexChar) {
if ( hexChar >= 'a' && hexChar <= 'z')
return 10+hexChar-'a';
else return hexChar - '0';
}
//特别注意 “10+hexChar-'a';”
选择流程
注意当判断条件含有某方法语句时,方法会重复运行,导致选择分支无法被选中,例如
outcome = judge();
if (outcome =="Win")
winCount++;
else (outcome =="Lose")
loseCount++;
而不是以下这样:
if ( judge( )=="Win")
winCount++;
else ( judge( ) =="Lose")
loseCount++;
数组下标的类型是什么
foreach循环
for (double e: mylist){
....
}
// 对于mylist数组中的每个e元素执行动作
将一连串空格符间隔的输入读取为数组
https://stackoverflow.com/questions/14635136/read-integers-separated-with-whitespace-into-int-array
输入控制:连续输入以空格间断的一组数字,以0为输入作为结束
public static String inputControl() {
Scanner input = new Scanner(System.in);
String userInput = input.next()+" ";
String user = "";
while( !userInput.equals("0 ") ) {
user += userInput;
userInput = input.next() + " ";
}
System.out.println( user );
return user;
}
输入: 1 2 3 2 1 0
返回user: "1 2 3 2 1"
// !!注意,循环条件中,字符串不能与0直接相等来比较;并且,注意"0 "有一个空格符;
也可讲条件改为 while( !userInput.trim().equals("0") )
类似的,要求以负数作为结束输入的标志,循环条件改为:
while( Integer.parseInt( userInput.trim()) >= 0 )
二分查找务必确保数组是按顺序排列
将两个有序数组合并为一个有序数组:交替比较法,Time O(length 1+length 2)
public static int[] mergeSorted(int nums1[], int nums2[]) {
int length1 = nums1.length, length2 = nums2.length;
int length = length1 + length2;
int index = 0, index1 = 0, index2 = 0;
while( index < length ) {
if ( nums1[index1] > nums2[index2]) {
nums[index] = nums2[index2] ;
index2++;
index++;
}
else {
nums[index] = nums1[index1] ;
index1++;
index++;
}
// if one of this two arrays has been filled,then copy leftover of the other
if ( index2 == length2 ) {
int leftInterval = length - index1 + 2;
for( int j = index; j < leftInterval; j++, index1++) {
nums[j] = nums1[index1];
}
break;
}
if ( index1 == length1 ) {
int leftInterval = length - index2 + 2;
for( int j = index; j < leftInterval ; j++, index2++) {
nums[j] = nums2[index2];
}
break;
}
}
return nums;
}
利用while循环和自增符来简化代码
public static int[] mergeSorted(int nums1[], int nums2[]) {
int length1 = nums1.length, length2 = nums2.length;
int length = length1 + length2;
int [] nums = new int[length];
int index = 0, index1 = 0, index2 = 0;
while (index1 < length1 && index2 < length2)
{
if (nums1[index1] < nums2[index2])
nums[index++] = nums1[index1++];
else
nums[index++] = nums2[index2++];
}
while (index1 < length1)
nums[index++] = nums1[index1++];
// Store remaining elements of second array
while (index2 < length2)
nums[index++] = nums2[index2++];
return nums;
}
分区
public static int[] partitionByElement(int nums[], int thisNum, int thisIndex) {
int length = nums.length;
int[] thisNums = new int[length];
int index = 0, lowIndex = 0, highIndex = length-1;
while( lowIndex < highIndex) {
if(nums[index] < thisNum) {
thisNums[lowIndex++] = nums[index];
}
else if( nums[index] > thisNum ) {
thisNums[highIndex--] = nums[index];
}
else if(nums[index] == thisNum && index != thisIndex) {
thisNums[lowIndex++] = nums[index];
}
index++;
}
thisNums[lowIndex] = thisNum;
return thisNums;
}
public static void partitionByElement(int[] nums, int pivotIndex) {
int pivot = nums[pivotIndex];
int length = nums.length;
//设计两个index位于两端,向pivot靠拢
int highIndex = length - 1;
int lowIndex = 0;
//检查两个index在循环体更新之后是否交叉
while(lowIndex < highIndex){
//找到不比pivot小的、最左边的元素的位置;包括等于pivot的元素或者就是pivot自身元素的位置
for(lowIndex = 0 ; nums[lowIndex] < pivot; lowIndex++);
//找到不比pivot大的、最右边的元素的位置;包括等于pivot的元素或者就是pivot自身元素的位置
for(highIndex = length - 1; nums[highIndex] > pivot; highIndex--);
if (lowIndex < highIndex ) {
int temp = nums[lowIndex];
nums[lowIndex] = nums[highIndex];
nums[highIndex] = temp;
}
}
}
// test case: the last 7 is the pivot
// test case1 {*,*,*,7,*,*,1,*} pass
// test case2 {*,7,*,*,7,1,*} pass
冒泡排序
public static int[] bubbleSort(int[] list) {
int[] arr = new int[list.length];
System.arraycopy(list, 0,arr, 0, list.length);
int n = arr.length;
boolean swapped;
for (int i = 0; i < n-1; i++) {
swapped = false;
//每个第i次外循环后,arr[n-1-i]及其后的元素都是升序排列,
//因为内循环能保证aar[n-i-1]比前面所有元素都大
//例如i=0的内循环完成后,arr[n-1]比前面所有元素都大
for (int j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = true;
}
}
//如果没有交换过,每个元素都比前一个大,也就是已经排好序
if (swapped == false)
break;
}
return arr;
}
//test case
**i = 0** , j < 6
1, 5, 4, 3, 9, 2, 1
j = 0 **1, 5**, 4, 3, 9, 2, 1
j = 1 1, **4, 5**, 3, 9, 2, 1
j = 2 1, 4, **3, 5**, 9, 2, 1
j = 3 1, 4, 3, **5, 9,** 2, 1
j = 4 1, 4, 3, 5, **2, 9,** 1
j = 5 1, 4, 3, 5, 2, **1, 9**
选择排序
public static void selectionSort(int[] nums){
for(int i = 0 ; i < nums.length-1; i++){
for(int j = i+1; j < nums.length; j++){
if( nums[i] > nums[j]){
int temp = nums[j];
nums[j] = nums[i];
nums[i] = temp;
}
}
}
}
二分查找
public static int binarySearch( int[] nums, int thisNum){
// the nums array should be sorted
int lowIndex = 0, highIndex = nums.length-1;
int mid = lowIndex;
while(highIndex > lowIndex){
mid = (highIndex + lowIndex) / 2;
if(nums[mid] > thisNum){
highIndex = mid-1; // 为什么要去mid-1 和 mid +1? 因为要确保查不到时highIndex能够大于lowIndex
}
else if(nums[mid] < thisNum){
lowIndex = mid+1;
}
else
return mid;
}
return -lowIndex-1; // 为什么还要减1? 因为lowIndex可能为0,不减1可能误认为nums[0]就是要找的数
}
循环
for-each循环
能够方便的遍历地选中每一个元素,但在循环体操作中不能改变选中的这个元素
字符串
字符串连接:
- 性能:在多次连接时(例如在循环中)将字符串创建为StringBuilder类的对象,而不是String;这样可以避免多次建立String对象。
二、时间
(一)日期
1. 获取并打印年月日、时分秒
- 使用
Date
类 ```java Date date = new Date(); //获取当前日期及时间的Date实例 SimpleDateFormat formatter= new SimpleDateFormat(“yyyy-MM-dd ‘at’ HH:mm:ss z”); System.out.println(formatter.format(date));
test:
2020-02-05 at 10:11:33 UTC #
- **注意**如何使用 `SimpleDateFormat` 类的 `format()`方法 格式化打印时间
- 使用 `Calendar`类
```java
Calendar calendar = Calendar.getInstance();//获取当前日期及时间的Calendar实例
SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
System.out.println(formatter.format(calendar.getTime())); //使用该实例的getTime()方法
- 使用
GregorianCalendar
类 ```java GregorianCalendar gCal = new GregorianCalendar(); elapsedMillis = System.currentTimeMillis(); gCal.setTimeInMillis(elapsedMillis); int year = gCal.get(GregorianCalendar.YEAR); int month = gCal.get(GregorianCalendar.MONTH); int day = gCal.get(GregorianCalendar.DAY_OF_MONTH); System.out.println( year+” “+(month+1)+” “+day )
//注意:month的0表示一月;
test:
2020 7 30 #
- 使用 `DateTime` API
- `LocalDate`类,只获取日期
```java
LocalDate date = LocalDate.now(); // 静态方法 now()获取当前日期
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
System.out.println(date.format(formatter));
#test
05-02-2020
#
注意如何使用
DateTimeFormattter
类的ofPattern
构造方法,format()
方法 格式化打印时间now()
方法还可以指定时区:LocalDate date = LocalDate.now(ZoneId.of("Europe/Paris"));
其中
ZoneId
的获取方式如下:System.out.println(ZoneId.getAvailableZoneIds());
localTime
类,只获取时间LocalTime time = LocalTime.now();
格式化打印和指定时区的方式与上类似
localDateTime
类LocalDateTime dateTime = LocalDateTime.now();
指定时区则是
ZonedDateTime
类ZonedDateTime dateTime = ZonedDateTime.now();
这两个
DateTime
类的格式化打印方式与上类似- 对于整个
DateTime
API,可以使用Clock
类的实例作为now()
方法的参数,进而指定某个时区(系统)下的日期时间 ``` //建立指定时区下的Clock实例 Clock clock = Clock.systemDefaultZone(); 指定系统的时区 Clock clock = Clock.systemUTC(); 指定UTC时区 Clock clock = Clock.system(ZoneId.of(“Europe/Paris”)); //用ZoneId指定时区
//用clock来建立如下四个类的实例 LocalDateTime localDateTime = LocalDateTime.now(clock); LocalTime localTime = LocalTime.now(clock); LocalDate localDate = LocalDate.now(clock); ZonedDateTime zonedDateTime.now(clock);
Instant instant = Instant.now(clock);
- Clock类还可以用实例方法获取当前毫秒数
```java
System.out.println(clock.millis());
//与如下两个语句相同
System.out.println(System.currentTimeMillis());
System.out.println(date.getTime());
- Clock类还可以用实例方法建立`Instant`类的对象
Instant instant = Instant.now(clock);
//或
Instant instant = clock.instant();
//打印instant的效果:
System.out.println(instant);
#
2020-04-16T17:05:43.867063Z
#
数值计算
数值表示范围溢出
Line1: BigDecimal thisNum = new BigDecimal(Long.MAX_VALUE+""+1);
Line2: BigDecimal thisNum = new BigDecimal(Long.MAX_VALUE+1+"");
数值计算是从左往右的,Line1
中先变为字符串在做字符串的连接;Line2
先加1会导致Long
数值类型的范围溢出。
抵消错误
当处理一个较大数和一个较小数时,可能会把较小数略去。
例如在计算 $1+\frac{1}{2}+\frac{1}{3}+…+\frac{1}{n}$时,从右到左计算更精确。
零除异常
用0除一个整型数(`int` `long` )会抛出零除异常;
但是用0(或 0.0)除一个浮点数( `float` `double`)不会有问题,因为 `IEEE 754`标准为浮点数设计了 `NaN` `+0` `-0` `-Infinity` `+Infinity`五个特殊数值,其中 `NaN`则用来表示 零除等浮点计算的结果。
类与对象
为什么要有类和对象?
程序中,常常会有多组数据产生联系,如果将各组数组抽象成数据域的各个”维度“,将各组数据的联系互动概括为方法域中的各个方法,那么这样的数据域和方法域就抽象成了一个类,而实际的数据及其互动就成了该类的实例,也即对象。
这样的抽象的好处在于:
- 实现了数据之间的紧密耦合,
- 实现了数据及其动作的紧密耦合,
紧密耦合就使得程序更便于设计和维护。
一、类的概念和描述
1. 概念
类是一种**引用类型**(区别于基本类型数据),意味着该类引用类型的变量都可引用该类的一个实例(对象)。
也即,引用变量沟通了了类和对象:
- 引用变量的类型表达“对象属于哪个类”;
引用变量的值表达“对象内容的起始位置”。(初始值为null,调用初始为null的对象内容发生
NullPointerException
)public class Test{
public static void main(){
java.util.Date[] dates = new java.util.Date[40]; //40个Date对象的数组,但此时只有40个Date类型的引用
System.out.println(dates[0]); //OK!只是打印第一个对象的引用值,并不涉及该对象的使用;
System.out.println(dates[0].toString()); //Error:NullPointerException. 该引用变量没有引用任何对象
}
}
也存在不被引用的对象,创建之后立即使用,称为匿名对象;匿名对象会在随后被垃圾回收(garbage collecting),若要让一个对象被回收,则要更改(引用别的对象 或者 改为null)所有引用该对象的引用变量的值。
2. 类图
+号表示public,-号表示private
下划线表示static
二、类的成员的可变性
按照是否被所有该类对象共享(都可访问、都可修改)的特点,数据域和方法可各自分为静态和实例两类。
静态数据域(**静态变量**)保存在公共的内存地址,其更改会涉及到该类所有的对象;
**静态方法**是无需创建类的实例就可调用的方法。
对于常量而言,一般将其设为静态常量。
静态与实例的权限:
静态方法和静态数据域不能调用实例方法、不能访问实例数据域,因为它们不属于具体的某个对象;
实例方法和实例数据域都可访问静态数据域、调用静态方法。
三、类及其成员的可见性:
1. 四种权限
- 默认类可以被同一包中其他类访问;
public
表示还可以被任意的,即别的包中的类访问private
表示只能被在自己的类中访问;只能修饰类的成员,不能修饰类名。protected
被某类访问,意思是在某类中访问此类的数据域和方法。
因此某类要访问别的类的数据域\方法,必须要考虑类本身和该数据域\方法两个层面的可见性。
如果要防止用户创建类的对象(例如此类的成员都是静态的),可以将构造方法修饰为
private
,例如
private Math(){
}
2. 数据域封装
为了**保护数据域安全**、**便于使用该类的程序维护**,通常将数据域修饰为 `private`,这样在定义该数据域的类以外,都不能对数据域直接修改。
若要修改,可以设置非 `private`可见性的设置方法(setter)和访问方法(getter)。
四、设置不可变的类和对象
什么是不可变:一旦创建之后其数据域和方法都不改变。
怎么才能满足不可变的要求:
- 所有数据域都是私有的:这样保证数据域在定义类之外都不能访问;
- 没有设置方法(修改器方法)
- 没有能够返回指向可变数据域的引用的访问方法
见P 306:
- ”返回引用“:在别的类中访问Student类的Date型数据的引用,然后就可以通过该引用值对该数据域做更改。
- “指向的数据域不能是可变的”:若访问方法指向的是String这种不可变数据域,即便访问到了也无法修改这种数据域;
五、类范围内的变量作用域
1. 回顾:方法中的变量(局部变量)作用域
方法中定义的变量称为局部变量,同一方法中的局部变量可以同名,但必须位于不同的块`{···}`中,这是因为局部变量是**块作用域**的:
- 局部变量的作用域大小与声明位置有关
方法签名中的变量(即参数)在整个方法块中都有效;
循环块头部的变量在整个循环中都有效;
循环块体中的变量在单次循环中才有效。 - 局部变量的初始化位置
初始化必须在一俩 该变量的方法 和 其他变量初始化 之前。
2. 类的变量(数据域)的作用域
类的变量即数据域,包含实例变量和静态变量。
- 类的变量的作用域大小始终是整个类,与声明位置无关
- 类的变量初始化位置
初始化可以在依赖该变量的方法之后,但必须在依赖此变量的其他变量初始化之前。
3. 局部变量与类数据域的优先级
由于类变量作用于整个类,因此不允许同名的类变量;
但允许在方法中局部变量与类变量同名,这是因为在方法中,局部变量优先于类变量,类变量被**隐藏**(而不是跟随局部变量改动)。
但注意:此时局部变量的优先仍然是基于块的。加入局部变量在某方法的某块中声明,那么在”方法内 块外“的位置,该局部变量无效,此时是类变量有效。见复习题9.13.1
一般,为了便于阅读,会将“方法的参数”这种局部变量与类变量同名;但别的情况下,要避免同名的局部变量出现。
六、this引用
`this`是用来引用**对象**自身的引用名。为了便于阅读,可以用 `this`显式表明数据域或方法所属于哪个对象。**注意**:当数据域或方法是静态的时候,最好依然是使用类名`Circle.angle`
但有两种情况必须要使用 `this`:
- 在类的方法(包括构造方法)中访问数据域时,若参数与类变量同名,会导致要被赋值的类变量会被隐藏:
public class Circle{
private double radius = 1;
private static double angle;
public void setRadius(double radius){
radius = radius;
}
}
因此用`this`来引用**对象**自身,把构造方法的赋值语句改为 `this.radius = radius;`
- 在构造方法中调用别的(重载)构造方法
此时this(arg-list);
语句应放在构造方法体的首行;
类与类之间的关系:
- public声明下的方法(含构造方法)称为公共方法,可以从不同包的其它类访问。
- private只能修饰方法和数据域,表示只能在自己类中访问
类与对象之间的关系:
- 引用:
- 引用变量的类型表达“对象属于哪个类”;
- 引用变量的值表达“对象内容的起始位置”。(初始值为null,调用初始为null的对象内容发生NullPointerException)
public class Test{
public static void main(){
java.util.Date[] dates = new java.util.Date[40]; //40个Date对象的数组,但此时只有40个Date类型的引用
System.out.println(dates[0]); //OK!只是打印第一个对象的引用值,并不涉及该对象的使用;
System.out.println(dates[0].toString()); //Error:NullPointerException. 该引用变量没有引用任何对象
}
}
七、类之间的关系
UML图的表示
以Student类和Course类为例,
- 图标:关联用一般实线连接,聚集在实线的被聚集一端加空菱形,组合在某一端加实心菱形。
- 数量表示:在类旁可以加上数量关系
- 角色表示:可以在类旁加上角色名称,动作名称以及表示该动作方向的箭头。
1. 关联
2. 聚集 与 组合
聚集表示了“类A有类B”的关系,例如Student和Address:一个学生可以有多个地址,但一个地址只能对应一个学生;
组合表示聚集的基础上,被聚集对象不能单独存在的关系:例如Student和Name,二者一一对应,可以说前者聚集于后者,或者后者聚集于前者,但Name类不能单独存在。
一般可统称为组合。
3. 继承
八、类的编译
- 类都能被编译,但有main方法的类(主类)才能被解释器运行
- public声明下的类称为公共类,可以从不同包的其他类访问。
- 一个Java文件中只能有一个公共类,文件内的类可以互相访问,这里的公共类不包括pubic的内部类
- Java文件名必须与公共类同名
- Java文件编译后形成多个.class文件,涉及到的Java文件外的公共类也会被编译。
时间
使用 GregorianCalendar类来打印年月日
GregorianCalendar d1 = new GregorianCalendar();
System.out.println(d1.get(GregorianCalendar.YEAR)+" "+d1.get(GregorianCalendar.MONTH)+
" "+d1.get(GregorianCalendar.DAY_OF_MONTH) );
常用类的笔记
(1)BigInteger和BigDecimal类
- 算术方法本身不能单独作为可执行语句,因为该方法是返回一个对象,而不是在原对象上更改
因此step.add(BigDecimal.ONE);
不能执行,而是step = step.add(BigDecimal.ONE);
因此BigDecimal nowNum = num.add(step);
执行后num
所指向的对象没有改变
(2)String类
快速创建一个给定长度的重复字符的字符串
String s ="*".repeat(x.length());
给定正则匹配模式,得到匹配的字串
三种匹配程度:matches
完全匹配:整个字符序列完全匹配成功,匹配成功了不再继续匹配。lookingAt
首位开始部分匹配:总是从第一个字符开始进行匹配,匹配成功了不再继续匹配。匹配失败了,也不继续匹配。find
灵活部分匹配:从当前位置开始匹配,找到一个匹配的子串,将移动下次匹配的位置。 ```java String s1 = “Chapter303..Chapter3_03..”; String regex1 = “Chapter[\d][\d]{2,}”; Pattern pattern1 = Pattern.compile(regex1); Matcher matcher1 = pattern1.matcher(s1); while(matcher1.find()) System.out.println(“find() Ok:”+matcher1.group()); while(matcher1.matches()) System.out.println(“matches() Ok:”+matcher1.group()); while(matcher1.lookingAt()) System.out.println(“lookingAt() Ok:”+matcher1.lookingAt());
//输出如下 find() Ok:Chapter3_03 find() Ok:Chapter3_03 lookingAt() Ok:Chapter3_03 lookingAt() Ok:Chapter3_03 lookingAt() Ok:Chapter3_03 lookingAt() Ok:Chapter3_03 //…(lookingAt的死循环)
- 如何多次实现字符串的部分替换<br />由于字符串是不可变的,要借助于`StringBuilder`或 `StringBuffer`,每次替换字符串时将引用原字符串对应的`StringBuilder`对象改为引用一个新的由新字符串创建的 `StringBuilder`对象。
```java
String name = "Chapter1_1/Chapter1_2/Chapter1_1.txt";
StringBuilder newName = new StringBuilder(name);
while(continueModify){
String mod = modify(name);
newName = new StringBuilder(mod);
}
return newName;
(3)Scanner 和 PrintWriter类
- 这两类对象的方法在循环中要特别注意重复抛出异常导致死循环:
//用InputMismatchException来测试
public static void Test(){
//随便定义一个5大小的数组
int[] nums = {0,1,2,3,4};
//设计循环
boolean inputAgain = true;
while (inputAgain) {
try {
//**如果把Scanner这个user对象放在循环之外,当user的nextInt()方法抛出一次异常,在以后的每次循环中的try{}块,user对象都会抛出该异常。因为该对象未更新。
Scanner user = new Scanner(System.in);
System.out.println(nums[user.nextInt()]);
}
catch (InputMismatchException ex) {
System.out.println("input a index(integer)");
inputAgain = true;
}
}
}
- 建立一个文件的
printWriter
对象后,只要这个对象对应的文件是真实存在的(file.exists() == true
),文本原内容会被擦除 - Scanner类的实例方法
hasNext()
:当文本中只有空白字符(回车符、换行符、换页符、Tab符)时,返回false.
基于标记和基于行的读取方法
基于标记的读取方法规则,以
input.next()
为例:
默认' '
空格字符为分隔符。该方法会首先跳过任意数量的分隔符(甚至没有分隔符),然后从第一个非分隔符的字符开始读取,一直到以第一个分隔符作为结束,称为标记。方法所返回的内容会去除标记中的这第一个分隔符。接着开始第二次读取与返回。
对于多行文本(包括键盘的多行输入)来说,该方法会忽略换行符。Scanner input = new Scanner(System.in);
String s1 = input.next();
String s2 = input.next();
System.out.print(s1+"!"+s2);
------------------------------如下以·代表输入中的空格分隔符
··s1··
··s2
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>输出
s1!s2
//所输入文本相当于 ··s1····s2,s1后面两空格作为s2前的“任意数量分隔符”被跳过了
基于行的读取方法规则,以
input.nextLine()
为例
该方法的读取由当前位置开始,由换行符结束。所返回的内容不包含作为结束的此换行符。Scanner input = new Scanner(System.in);
String s1 = input.next();
String s2 = input.nextLine();
System.out.print(s1+"!"+s2);
------------------------------如下以·代表输入中的空格分隔符
··s1··
(··s2)来不及输入第二行
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>输出
s1!··
//当输入第一行后按下回车,输入文本相当于··s1··'\n'。nextLine()开始读取时“当前位置”就在s1后的第一个空格符,一直到按下回车。
因此不应在基于标记的输入后使用基于行的输入,不管这两次输入间隔了多少其他代码。
(4)File类
1. 目录操作
mkdir()
方法要保证父目录是存在的,否则既不会输出true
也不会输出false
;若不管父目录存在与否,用mkdirs()
方法。isFile()
方法判断为 true 只需要所给路径是在系统中是真实存在的:- 可以识别父级目录甚至其他系统盘,例如
c:/
。(在任何目录中,/
不是必须但对于系统盘:
是必须的) - 也可以识别子目录,例如
Chapter
- 表示当前目录,用
.
- 可以识别父级目录甚至其他系统盘,例如
- 在保持目录下不变的情况下,也可以用
thisDir.reNameTo(File destDir)
方法重命名该目录;但不能通过改文件名重设路径来改变目录名。 File[] files = dir.listFiles();
在对files
的使用中可能会产生NullPointerException
。这是因为dir
可能不是一个目录型File
对象,或者在使用时可能产生IO异常。一般解决办法是用if(files == null)
排除,目录为空不是files == null
,一个大小为空的File[]
2. 文件操作
thisFile.reNameTo(File destFile)
方法需要thisFile
对象必须真实存在(即建立这个对象所用的Path
是真实正确的);而对于dest
对象,它所代表的path
可以带别的目录(即移动此文件),也可以更改文件属性名;但是当该目录下有同名同属性文件则重命名失败。
异常
异常处理的结构,即:异常声明+异常处理(异常抛出+异常捕获+尾部处理)
public class Test {
public static void main(String[] args) throws Exception {
try {
method();
System.out.println("After the method call:");
}
catch (RuntimeException ex){
System.out.println("RuntimeException in main");
}
catch (Exception ex){
System.out.println("Exception in main");
}
}
static void method() throws Exception{
try{
String s ="abc";
System.out.println(s.charAt(3));
System.out.println("ah ha");
}
catch (RuntimeException ex){
System.out.println("RuntimeException in method");
}
catch (Exception ex){
System.out.println("Exception in method");
}
finally {
System.out.println("Finally");
}
}
}
异常的种类
Throwable
类派生出 Error
(系统错误)类 和 Exception
类;而 RuntimeException
类继承自 Exception
类。这三类异常都发生在系统运行时;而 Error
和 RuntimeException
称为 免检异常, Exception
及其子类称为必检异常。
异常声明
若某方法可能会产生异常,就会在方法头声明该异常类 或 其父异常类;对于只可能产生免检异常的,不必显式声明。
若异常在某方法中抛出但未被处理 或 处理了但未被捕获,异常将传递给调用该方法的方法;因此,调用该方法的方法也应做异常声明。
若异常在父类方法中未作异常声明,则当子类在可继承该方法甚至重写、重载时也不能作异常声明。
异常处理
- 由
try{}
catch{}
finally{}
语句块顺序构成,且语句块之间不能插入其他语句。 try
块是为了显示告知编译器哪些语句会抛出异常,但是当这些语句仅抛出免检异常,可以省去try
块,编译器依然可以自动识别出是哪种异常,然后创建该类异常并抛出给catch
块们匹配。catch
块的设置要注意,子类异常在前父类异常在后,避免异常捕获(即异常对象参数的匹配)中出现歧义,导致错误。finally
块和catch
块至少要有一个,作为try
块的handler
。
try{
statement1;
statement2;
}
catch(RuntimeException ex){
handle1_1;
handle1_2;
}
catch(Exception ex){
handle2_1;
throw ex;
handle2_2;
}
finally{
endOperation;
}
restStatemnt;
执行顺序
- 没有出现异常
依次执行try
块语句,若没有抛出异常,直接执行fianally
块及余下语句。 - 出现异常,并捕获
依次执行try
块语句,当statement1
抛出异常(可能自身抛出,也可能所调用的方法抛出),会立即匹配catch
块,之后都不再执行try
块余下语句;
而此时例如被第二个catch
块捕获并执行handle2_1
和handle 2
,最后再执行fianally
块 及余下语句。
此时可以抛出异常- 可抛出的异常类型(原类型
Exception
):- 可再抛出捕获的这个异常
throw ex
, - 也可重新抛出同类的异常
throw new Exception("exception message",ex)
,第二个参数ex
表示此异常与ex是链式关系,若没有这个参数就是独立的新的异常对象 - 或者重新抛出其派生类的异常
throw new RuntimeException("exception mesage")
,此时不能设置ex
参数,即不能与原异常构成链式关系
- 可再抛出捕获的这个异常
handle 2
被跳过,直接执行finally
块,余下语句也被跳过,直接返回到调用该方法的方法。catch
块 只能抛出捕获的这个异常,不能创建某个异常并抛出。
- 可抛出的异常类型(原类型
- 出现异常,未捕获
依次执行try
块语句,当statement1
抛出异常,但没有匹配任何catch
块,也会执行fianally
块,之后会省略余下语句直接将异常传递给本方法的调用者,一直到main
方法,若最后仍未被捕获,会导致系统中断。
可见:
1.try
语句块中,抛出异常的语句后余下的都不执行;
2. 无论如何,即使在其之前有return
语句,finally
块都会执行;
3. 当且仅当”出现异常但未捕获“ 或 “捕获并继续抛给所调用方法”时,catch-finally
后的语句会被省略
正则表达式
Trick
- 判断数组列表或字符串中某元素的唯一性:
用`if( indexOf(e) == lastIndexOf(e) )