Java suppressed exceptions

public class Main {
    public static void main(String[] args) {
        doSomething();
    }
    public static void doSomething(){
        try(MyAutoCloseable ac = new MyAutoCloseable()){
            ac.saySomething();
        } catch (IOException e) {
            System.out.println(e.getClass().getSimpleName() + " - " + e.getMessage());
        }
    }
}
public class MyAutoCloseable implements AutoCloseable {
    public void saySomething() throws IOException{
        throw new IOException("Exception from saySomething");
    }
    @Override
    public void close() throws IOException {
        throw new IOException("Exception from close");
    }
}

try with resources 语法中遇到嵌套的throw,里面的异常就是被抑制的异常,suppressed exception,我们可以看一眼反编译的代码,可以发现编译器做了很多工作

public static void doSomething() {
    try {
        MyAutoCloseable ac = new MyAutoCloseable();

        try {
            ac.saySomething();
        } catch (Throwable var4) {
            try {
                ac.close();
            } catch (Throwable var3) {
                var4.addSuppressed(var3);
            }

            throw var4;
        }

        ac.close();
    } catch (IOException var5) {
        PrintStream var10000 = System.out;
        String var10001 = var5.getClass().getSimpleName();
        var10000.println(var10001 + " - " + var5.getMessage());
    }

}

所以为了看到完整的异常,我们可以使用getSuppressed方法

public static void doSomething(){
    try(MyAutoCloseable ac = new MyAutoCloseable()){
        ac.saySomething();
    } catch (IOException e) {
        System.out.println(e.getClass().getSimpleName() + " - " + e.getMessage());
        for (Throwable t:e.getSuppressed()){
            System.out.println("Suppressed: " + t.getMessage());
        }
    }
}

看一个更复杂的例子

public static void doTryWithResourcesMulti() {
    char[] buff = new char[8];
    int length;
    try (Reader reader = Helper.openReader("data/file.txt");
         Writer writer = Helper.openWriter("data/file2.txt")) {
        while ((length = reader.read(buff)) >= 0) {
            System.out.println("\nlength: " + length);
//                for (int i = 0; i < length; i++) System.out.print(buff[i]);
            writer.write(buff, 0, length);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
public static void doTryWithResourcesMulti() {
    char[] buff = new char[8];

    try {
        Reader reader = Helper.openReader("data/file.txt");

        try {
            Writer writer = Helper.openWriter("data/file2.txt");

            int length;
            try {
                while((length = reader.read(buff)) >= 0) {
                    System.out.println("\nlength: " + length);
                    writer.write(buff, 0, length);
                }
            } catch (Throwable var8) {
                if (writer != null) {
                    try {
                        writer.close();
                    } catch (Throwable var7) {
                        var8.addSuppressed(var7);
                    }
                }

                throw var8;
            }

            if (writer != null) {
                writer.close();
            }
        } catch (Throwable var9) {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Throwable var6) {
                    var9.addSuppressed(var6);
                }
            }

            throw var9;
        }

        if (reader != null) {
            reader.close();
        }
    } catch (IOException var10) {
        var10.printStackTrace();
    }

}

在IIS服务器上安装和配置Web Deploy

前言

烂文警告,写下这篇文章只是希望这折腾的两个多小时更有一些意义,学到的东西可以转化。将来如果用来部署其他有用的程序之时会有帮助。

背景

省略吐槽Y姓老师500字,要部署一个WebService程序到我的服务器上。参考了一篇比较靠谱的文章
环境是WinServer19 DataCenter + IIS10 + VS2019

步骤

因为结果是试出来的,原理不懂无法展现正确的步骤了,可以参考上面的那篇文章。讲一下理论上的步骤和最后的截图。

我习惯直接开“控制面板”点“启用或关闭Windows功能”直接能跳到“添加角色和功能向导”,添加“Web服务器(IIS)”下面管理工具里的“管理服务”。(因为目标这个程序过于老旧(感谢同学借我抄作业),我添加了.NET 3.5 来获得 V2.0 的CLR)

开启管理服务

那个 Web Platform Installer 加载就要一辈子,所以我们老老实实去下载Web Deploy,并安装。

自己的经历是这样的,大概先安装了Web Deploy后开启的管理服务。导致右键网站节点并没有部署(重启管理界面和重启服务器都没有出现),去控制面板更改才发现了它不是真正的完全安装,根据描述可以推测可能因为没有开启管理服务,所以没有安装这个组件。之后我重装了,选的手动,就把这些功能全装上了,根据功能描述,没有这个处理程序应该是不能远程部署的。(结论比较模糊,我懒得做实验了)你安装的时候注意一下这里就行。

并不是真正的完整安装

装完上面两样之后,看图说话吧,IIS根节点下面找管理程序,如果没有开启就开启它,双击进入

打卡管理服务

端口自己看着改,只要是防火墙能通过的就行,如果默认的防火墙也能过那就默认的。然后启动服务。左边的IP地址,我改成服务器的公网IP是启动不了的,不懂,就选全部未分配了。

开启远程连接

对你要部署的网站右键,如果没有部署图标(先关闭IIS管理器再开一下),建议看看上面提到的坑。记得修改URL,端口应该要和管理服务里的一样,我域名用IP,端口就如上图的4567,这些都是小问题,自己尝试一下就行。

启用Web Deploy
记得修改URL

VS里的操作,在项目上点击发布,导入设置,最后编辑设置测试一下能否连接,能链接之后发布就成。

尾巴

烂文完,下次一定认真写。

UCF Local Programming Contest 2012(Practice)2020/03/04 计蒜客

题目地址 https://www.jisuanke.com/contest/7332?view=challenges

H. Ordered Numbers

就三个数的排序,原样和排序后的输出一遍。

printf("   Original order: %d %d %d\n", a, b, c);
if (a > b) swap(a, b);
if (a > c) swap(a, c);
if (b > c) swap(b, c);
printf("   Smallest to largest: %d %d %d\n\n", a, b, c);

B. How Old Are You Mr.String?

两个字符串比大小,只不过是先看z出现次数,再y出现次数,以此类推,具体看题,自己被getline坑,应该是他们行末的空格?<-这个坑回来再填

string a,b;
cin>>a;
sort(a.begin(),a.end(),cmp);
cin>>b;
sort(b.begin(),b.end(),cmp);
cout<<"Data set #"<<i<<": ";
if (a>b) cout<<"First string is older\n\n";
else if (a<b) cout<<"First string is younger\n\n";
else cout<<"The two strings are the same age\n\n";

C. Clean Up the Powers that Be

题目说了因子已经是素了的,所以拿map加一下幂就好了,就是格式化得注意一下

void space(int a){
// 算空格个数
	int s=0;
	while(a>0){
		a=a/10;
		s++;
	}
	for (int i=0;i<s;i++) printf(" ");
}
int main(){
	//..
	map<int,int> mymap;
	for (int i=0;i<n;i++){
		int a,b;
		scanf("%d %d",&a,&b);
                // 真就拿map加一加
		if (mymap.find(a)==mymap.end()) mymap[a]=b;
		else mymap[a]+=b;
	}
	printf("Prime Factorization #%d:\n",k);
	for (auto &item:mymap){  // C++11 auto 迭代STL容器
	// 没有&是值传递,不能修改 加了&是引用就可以改啦 虽然我这里没有修改
		space(item.first);
		printf("%d",item.second);
	}
	printf("\n");
	for (auto &item:mymap){
		printf("%d",item.first);
		space(item.second);
	}
	printf("\n\n");
	//..
}

G. Lifeform Detector

<s> 有 三种模式 : a<t>b<s>  或者 c<s> 或者 空
<t> 有两种模式: a<t>b<s> 或者 c<s>

将<t>,<s> 多带一带就会发现它其实是个括号匹配问题 c随意,a 左括号,b 右括号
根据题解少考虑了ab不能连起来的问题

bool judge(string s){
	int top=0;
	for (int i=0;i<s.size();i++){
		if (s[i]=='c') continue;
		if (s[i]=='a') top++;
		else top--;
		if (top<0) return false;
	}
	if (top>0) return false;
	return true;
}

D. The Clock Algorithm

操作系统页面置换算法LRU变种Clock算法,就模拟,题目大致翻译可以参考。

操作系统设计中有一项技术叫做虚拟内存,允许计算机运行一个需要内存大于可用物理内存的程序,内存被分成页(pages),是固定大小的。
程序申请访问一个没有被加载到内存的页,会出现缺页中断(page falut) (意味着每页的第一次访问必然出现缺页)
如果程序需要访问另外一个内存可以容纳的页(假设它原本不在其中),之前加载的某一页必须被换出(swap),写到磁盘上,这个问题中我们讨论一种LRU的变体 时钟算法 Clock algo.
这种算法这样工作:
最初内存里n个 page cell 都是空的 (可以放page的格子,最多放n页的意思) 可以想象成一个大小为n的数组
另外,时钟算法持有一个 “hand pointer” 最初指向 cell 1(第一个空格子),我们后面就管他叫指针

现在我们假设每一页还有一个flag,程序需要访问某一页的时候,他检查这页有没有在内存里(有没有在这些格子里),如果在内存里,这一页的flag变成new;如果没有在内存里,把这一页加载到下一个为空的cell里面,并设置新加载的这页的flag为new

如果没有空的cell (因为只有n个cell,放满了) 我们就找hand pointer 指向的那个cell,如果那一页的标记是old。我们就把他换成我现在请求的这一页,新加载的flag标记成new,指针向前移动一位(循环的 n的一下一位就是1,可以用0..n-1)

所以新加载进来的一定的flag是new。

然后这个算法给了一个单元个两次机会 也就是本来是new 变成old 转回来他还是old的时候他就要换出去了

然后如果他被标记成old,但没有被换出(有之前的old被换)再一次请求他就会变成new

(最后实现了LRU的变体 LRU指的就是 最久没被访问的换掉,LRU中文翻译很蠢就不说了)

输入
n,r;n是可用的内存块(page cell) r 是请求内存的次数
接下来是r个整数 代表着请求的页号
以 0 0结束

输出
一开始是Program p
对于每个请求
如果 ri 没有在内存里,输出 Page ri loaded into cell c
如果在内存里 输入Access page ri in cell c
最后输出 There are a total of k page faluts k次缺页
换行

不如看看代码?

int pageincell[60];  // cell里存放的page的编号
int page2cell[60];   // page对应的cell 如果page 不存在就是-1
bool flag[60];       // cell的标记
int main(){
	int n,r;
	int t=0;
	while (~scanf("%d%d",&n,&r)){
		if (n==0 && r==0) break;
		printf("Program %d\n",++t);
		for (int i=0;i<60;i++) {
			pageincell[i]=55;
			page2cell[i]=-1;
			flag[i] = false;
		}
		int page;
		int hand=0;   // 指针
		int falut=0;  // 缺页次数
		for (int i=0;i<r;i++){
			scanf("%d",&page);
			if (page2cell[page]!=-1){ // 如果没缺页
				int cell = page2cell[page];   // 找到它的cell
				flag[cell] = true;            // 刷新成new
				printf("Access page %d in cell %d.\n",page,cell+1);  //输出
			}
			else{  // 缺页的话 找一个可以放的 一开始空的n个可以认为n个old的格子
				while(flag[hand]){
					flag[hand]=false;
					hand=(hand+1)%n;
				}
				page2cell[pageincell[hand]]=-1;  // 原来cell对应的page的置换出去
				pageincell[hand] = page;         // cell 放入这个page
				page2cell[page] = hand;          // 当前page 指向cell的位置
				flag[hand]=true;                 // flag刷新成new
				falut++;                         // 中断次数+1
				hand=(hand+1) % n;               // 指向下一个
				printf("Page %d loaded into cell %d.\n",page,hand+1);
			}
		}
		printf("There are a total of %d page faults.\n\n", falut);
	}	
}

A. Wall Street Monopoly

就区间DP,很明显,找一点区间DP的题先练练

struct node
{
	int length,depth,cost;  // 长 深 花费
};
node dp[25][25];  //dp[i][j] 表示的是i到j合并 所需要最小的花费,以及他们合并后的方块的长 深
int calc(int n){
	for (int len=1;len<n;len++){  //有len+1个相邻的格子合并,len=0 也就是一个格子自己合并初始话的时候就做了
		for (int i=0;i<n-len;i++){  // 枚举左端点
			int j = i+len;      // 确定右端点
			node &res = dp[i][j];  // 只要胆子大,引用爽到家,注意引用不能再次指向到另一个对象
			res.cost = -1;   // 一开始做个标记
			for (int k=i;k<j;k++){ // 确定中间谁分开
				// 左边合并的费用 加上右边合并的费用 加上这次合并的费用
				int val = dp[i][k].cost + dp[k+1][j].cost;
				// 这次合并的费用是左右两方块 各自比较长的边的积
				int val += min(dp[i][k].length,dp[i][k].depth)*
					min(dp[k+1][j].length,dp[k+1][j].depth);
				// 如果是第一次算出的结果,或者是比最小值小 那么
				if (res.cost==-1 || res.cost>val){
					// 根据题意,新合成的格子 长度相加 深度取二者较大的
					res.length = dp[i][k].length + dp[k+1][j].length;
					res.depth = max(dp[i][k].depth,dp[k+1][j].depth);
					res.cost = val;
				}
			}
		}
	}
	return dp[0][n-1].cost;
}
int main(){
	int t;
	cin>>t;
	for (int k=1;k<=t;k++){
		int n;
		cin>>n;
		for (int i=0;i<n;i++) {
			cin>>dp[i][i].length>>dp[i][i].depth;
			dp[i][i].cost=0;   //初始化,自己合并自己的不要钱
		}
		cout<<"The minimum cost for lot #"<<k<<" is $"<<100ll*calc(n)<<".\n\n"; 
		// 
	}	
}

F. Metallic Equipment Rigid

c个圆,p个点组成的路径,问路径接触了哪些圆圈,判断圆心到线段的距离就行,注意判断下面这种情况,周花的图(代码也是根据周的思路)

高虽然比半径小,但线段没有接触圆
double dis(point a, point b, circle o) {
	double ab = sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
	double oa = sqrt((a.x - o.x) * (a.x - o.x) + (a.y - o.y) * (a.y - o.y));
	double ob = sqrt((o.x - b.x) * (o.x - b.x) + (o.y - b.y) * (o.y - b.y));
	if ((oa * oa + ab * ab - ob * ob) / oa / ab / 2 <= 0) return  oa;  // 余弦定理,cos小于0说明是钝角
	if ((ob * ob + ab * ab - oa * oa) / ob / ab / 2 <= 0) return  ob;
	double p = (ab + oa + ob) / 2;  
	return sqrt(p * (p - ab) * (p - oa) * (p - ob)) * 2 / ab; // 这个是海伦公式,算面积的,把面积乘以2除以bc来算高
}

E. Pete’s Pantry

大模拟

使用SQLite保存数据

本文基本内容是对于官方文档Save data using SQLite的瞎JB翻译,以及优达学城安卓基础最后一门数据存储学习的记录。SQL过于底层,官方推荐Room库。

这篇文章里有两条时间线,一条是官网文档里的样例代码FeedReeder,还有一条是Udacity的课程项目Pet。

Step 1. Define a schema and contract

Schema 是SQL数据库的原则之一,他定义了数据库如何组织数据。Schema也反映在创建数据库的SQL语句中。您可能会发现创建一个伴随类(称为 contract class)会很有帮助,该类系统且明确地指定了schema的层次布局。

一些常量定义了URI,表和列的名称,contract类正是这些常量的容器。您可以在同一包的所有其他类中使用contract类中定义的常量。这样一来,修改表结构时,您只需在contract类中更改常量的值即可。

组织contract类的一种好方法是将数据库的全局定义放在类的根级别。然后为每个表创建一个内部类。每个内部类枚举相应表的列。

第一步大概的意思就是你得确定数据库的架构,写一个Contract类来体现这种架构(这不是必须的,但是写了会方便)。举一个栗子,裸写一个SQL语句大概长这样

String makeTableStatement = "CREATE TABLE entry(
			_id INTEGER PRIMARY KEY,
			entryid TEXT,
			title TEXT,
			sbutitle TEXT);";

然后看文档里的案例,我们先写一个contract类,现在只需要知道这里定义了一些常量。

// FeedReaderContract.class
public final class FeedReaderContract {
    // 防止被实例化,私有化构造函数
    private FeedReaderContract() {}

    /* 内部类定义表的内容,实现BaseColumns这个接口的时候
    会继承一个叫做 _ID 的主键 */
    public static class FeedEntry implements BaseColumns {
        public static final String TABLE_NAME = "entry";
        public static final String COLUMN_NAME_TITLE = "title";
        public static final String COLUMN_NAME_SUBTITLE = "subtitle";
    }
}

我们用上面那些常量再写一个SQL语句(一般会写在帮助类里面)

// 一般存在于 步骤2 会讲到的帮助类中
String SQL_CREATE_ENTRIES = "CREATE TABLE " +
		FeedEntry.TABLE_NAME + " (" +
		FeedEntry._ID + " INTEGER PRIMARY KEY," +
		FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
		FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT);";

看上去便麻烦了,但是有智能提示啊!而且当你想修改某个字段时,只需要修改常量的值就可以。如果你不用常量,就得修改整个代码中所有用到这个字段的地方。毕竟我没那个神仙水平看得懂aaabbb000111ttuuvvbtn1234567

总结一下contract类的三个用处

  • 1、帮助定义了(体现了)schema,在这里定义了常量,我们可以方便地找到这些常量
  • 2、减少在SQL语句中的拼写错误
  • 3、便于更新

接下来,阅读代码,回答以下问题 (这个是课程中的一道习题)

  • 1.这个天气应用数据库里有多少张表?
  • 2.WeatherEntry对应的表,在SQLite数据库中的表名是什么?
  • 3.问题2那张表中有多少列?(不包括_ID和_COUNT)
  • 4.有一个常量对应的字段是weather condition的short description,这个常量是什么?
  • 2张表,58行位置表,90行天气表
  • weather,100行
  • 10,103-127行
  • COLUMN_SHORT_DESC,在111行

下面是课程中Pet项目的一些操作

create a contract class
添加一个Contract类
package com.example.android.pets.data;

import android.provider.BaseColumns;

public final class PetContract {
    private PetContract() {
    }

    public static abstract class PetEntry implements BaseColumns {
        public static final String TABLE_NAME = "pets";
        public static final String _ID = BaseColumns._ID;
        public static final String COLUMN_PET_NAME = "name";
        public static final String COLUMN_PET_BREED = "breed";
        public static final String COLUMN_PET_GENDER = "gender";
        public static final String COLUMN_PET_WEIGHT = "weight";

        public static final int GENDER_UNKNOWN = 0;
        public static final int GENDER_MALE = 1;
        public static final int GENDER_FEMALE = 2;
    }
}
readability
在其他类中,使用常量,增加可读性

Step 2. Create a database using an SQL helper

SQLiteOpenHelper类包含一组有用的API,用于管理数据库。当您使用此类获取对数据库的引用时,系统仅在需要时才执行创建和更新数据库的操作(可能花费很长时间运行),而不是在应用程序启动期间执行。您需要做的就是调用getWritableDatabase()或getReadableDatabase()。

要使用SQLiteOpenHelper,请创建一个重写onCreate()和onUpgrade()回调方法的子类。您可能还想实现onDowngrade()或onOpen()方法,但不是必需的。

// 1. 创建一个类继承 SQLiteOpenHelper
public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // 2. 为数据库名称和数据库版本创建常量
    // 如果你修改了数据库 schema, 你必须增加 database version 的值.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "FeedReader.db";

    // 还记得上面我们用常量写的SQL语句嘛,就是这里的一部分
    // 记得import FeedReaderContract.FeedEntry
    private static final String SQL_CREATE_ENTRIES =
            "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
                    FeedEntry._ID + " INTEGER PRIMARY KEY," +
                    FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
                    FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)";

    private static final String SQL_DELETE_ENTRIES =
            "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;

    // 3. 写一个构造函数
    public FeedReaderDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    // 4. 实现 OnCreate 回调函数
    public void onCreate(SQLiteDatabase db) {
        // void execSQL(String sql) 
        //执行,无返回值,不能用于查询等
        db.execSQL(SQL_CREATE_ENTRIES);
    }

    // 5. 实现 onUpgrade 回调函数
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 这个数据库只是在线数据的缓存,
        // 升级策略就是删掉数据,从头来过
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}

类似的,实现PetDbHelper类

public final class PetDbHelper extends SQLiteOpenHelper {

    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "shelter.db";

    private static final String SQL_CREATE_ENTRIES =
            "CREATE TABLE " + PetEntry.TABLE_NAME + "( " +
                    PetEntry._ID + " INTEGER PRIMARY KEY," +
                    PetEntry.COLUMN_PET_NAME + " TEXT," +
                    PetEntry.COLUMN_PET_BREED + " TEXT NOT NULL," +
                    PetEntry.COLUMN_PET_GENDER + " INTEGER NOT NULL," +
                    PetEntry.COLUMN_PET_WEIGHT + " INTEGER NOT NULL DEFAULT 0);";
    private static final String SQL_DELETE_ENTRIES =
            "DROP TABLE IF EXISTS " + PetEntry.TABLE_NAME;

    public PetDbHelper(Context context){
        super(context, DATABASE_NAME,null,DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(SQL_CREATE_ENTRIES);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        sqLiteDatabase.execSQL(SQL_DELETE_ENTRIES);
        onCreate(sqLiteDatabase);
    }
}

没有权限就先复制一个临时的类,用来展示信息,仔细阅读代码就知道怎么操作数据库了

    /**
     * Temporary helper method to display information in the onscreen TextView about the state of
     * the pets database.
     */
    private void displayDatabaseInfo() {
        // To access our database, we instantiate our subclass of SQLiteOpenHelper
        // and pass the context, which is the current activity.
        PetDbHelper mDbHelper = new PetDbHelper(this);

        // Create and/or open a database to read from it
        SQLiteDatabase db = mDbHelper.getReadableDatabase();

        // Perform this raw SQL query "SELECT * FROM pets"
        // to get a Cursor that contains all rows from the pets table.
        Cursor cursor = db.rawQuery("SELECT * FROM " + PetEntry.TABLE_NAME, null);
        try {
            // Display the number of rows in the Cursor (which reflects the number of rows in the
            // pets table in the database).
            TextView displayView = (TextView) findViewById(R.id.text_view_pet);
            displayView.setText("Number of rows in pets database table: " + cursor.getCount());
        } finally {
            // Always close the cursor when you're done reading from it. This releases all its
            // resources and makes it invalid.
            cursor.close();
        }
    }

Step 3. CRUD

3.1 Create

// Gets the data repository in write mode
// 数据库不存在时会调用dhHelper中的onCreate方法,返回一个可写的数据库对象
SQLiteDatabase db = dbHelper.getWritableDatabase();

// Create a new map of values, where column names are the key
// 创建键值对的映射
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);

// Insert the new row, returning the primary key value of the new row
// 插入新的一行数据,返回新纪录主键的值
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);

向数据库插入一条记录,只需向insert方法传递一个ContentValues对象。

很明显insert()方法的第一个参数是表名。第二个参数告诉框架当ContentValues内容为空时要做什么(比如你没有放任何键值对进去)。If you specify the name of a column, the framework inserts a row and sets the value of that column to null. If you specify null, like in this code sample, the framework does not insert a row when there are no values.(反正我看不懂他什么意思,框架源码的链接可能有帮助,insertWithOnConflict方法,和SQLiteStatement的父类SQLiteProgram)

insert()方法会返回新插入行的ID,或者在插入失败时返回-1.

3.2 Read

        // Create and/or open a database to read from it
        SQLiteDatabase db = mDbHelper.getReadableDatabase();
        // 写一个投影(选择列)
        String[] projection = {
                PetEntry._ID,
                PetEntry.COLUMN_PET_NAME,
                PetEntry.COLUMN_PET_BREED,
                PetEntry.COLUMN_PET_GENDER,
                PetEntry.COLUMN_PET_WEIGHT
        };
        // 用query()方法来选择,前四个参数比较重要,具体请参见官方文档
        // 三四两个参数用来选择行一般就是where,且用来访防止sql注入
        // 看到我敷衍的语气了嘛,这篇文章估计马上就要咕咕咕了
        Cursor cursor = db.query(PetEntry.TABLE_NAME,
                projection,
                null,null,null,null,null);
        try {
            // do something with cursor
        } finally {
            // Always close the cursor when you're done reading from it. This releases all its
            // resources and makes it invalid.
            cursor.close();
        }

3.3 Update

3.4 Delete

更新和删除也差不多,然而我直接奔向了Content Provider,害,啥时候能上Room啊。