Code Clone Analysis Method for Practical Refactoring Support Yoshiki Higo*, Toshihiro Kamiya**, Shinji Kusumoto*,Katsuro Inoue* *Graduate School of Information and Science Technology, Osaka University **
Download
Report
Transcript Code Clone Analysis Method for Practical Refactoring Support Yoshiki Higo*, Toshihiro Kamiya**, Shinji Kusumoto*,Katsuro Inoue* *Graduate School of Information and Science Technology, Osaka University **
Code Clone Analysis Method for
Practical Refactoring Support
Yoshiki Higo*, Toshihiro Kamiya**,
Shinji Kusumoto*,Katsuro Inoue*
*Graduate School of Information and Science Technology, Osaka University
** PRESTO, Japan Science and Technology Agency
2020/4/26
電子情報通信学会 研究報告
1
研究の背景
コードクローンとは
ソースコード中に存在するコード片で、同形のコード
片が他に存在するもの
コピー&ペーストによるプログラム再利用などで生
じる
ソフトウェア保守を困難にする
あるコード片にバグがあると、そのコードクローン全
てについて修正の検討をしなければならない
機能を追加する場合も同様のことが言える
2
コードクローンに対する
保守支援
1.
コードクローンの量,分布状態などの把握
全てのクローンに対してもれなく修正・機能追加を行う.
2.
コードクローンの集約(リファクタリング)
将来的な修正コストの削減
リファクタリングとは,ソフトウェアの外部的振る舞い
を保ったままで,内部の構造を改善していく作業
2020/4/26
現在までにさまざまなリファクタリングパターンが提案されて
いる
重複したコード(コードクローン)はもっとも優先してリファクタ
リングすべき,といわれている
電子情報通信学会 研究報告
3
目的に応じたコードクローン検出
1. ソースコード中のクローンの量,分布状
態の把握
コードクローンを漏れなく検出する
ことが望ましい
2. クローンの集約(リファクタリング)
集約が可能であり,かつ,集約の
効果が大きいコードクローンを検
出することが望ましい
2020/4/26
電子情報通信学会 研究報告
4
集約を目的としたクローン検出
プログラム依存グラフ(PDG)上での類似部分の抽出[1]
非常に精度が高い
PDG構築の計算量が多い(O(n2) : nはソースコード中の文や式の
数)
メトリクスを用いて関数・メソッド単位の類似部分を抽出[2]
クローン検出は21種類のメトリクスを用いる
検出対象が関数・メソッドのみ
[1] R. Komondoor and S. Horwitz, “Using slicing to identify duplication in source code”, In Proc. of
the 8th International Symposium on Static Analysis, Paris, France, July 16-18, 2001.
[2] Magdalena Balazinska, Ettore Merlo, Michel Dagenais, Bruno Lague, and Lostas Kontogiannis,
“Advanced Clone-Analysis to Support Object-Oriented System Refactoring”, WCRE 2000, pp. 98-107
研究内容
本研究では,リファクタリングを目的としたクロー
ン検出・集約手法を提案する
コードクローン検出
高いスケーラビリティ
細粒度のクローンを検出
集約方法の提示
2020/4/26
メトリクスを用いたコードクローンの特徴づけ
電子情報通信学会 研究報告
6
コードクローン検出
概要
目標
高いスケーラビリティ
(それほど高い計算コストを必要としない)
細粒度のクローンも検出
言語における構造的なまとまりをもったク
ローンを高速に検出
2020/4/26
電子情報通信学会 研究報告
7
クローンペアとクローンセット
クローンペア: 同形のコードクローンの対
クローンセット: 同形のコードクローンの集合
C1
C2
C3
クローンペア クローンセット
(C1, C4)
{C1, C4, C5}
(C1, C5)
{C2, C3}
(C2, C3)
(C4, C5)
C4
C5
8
クローン検出
手順
コードクローン検出ツールCCFinderを利用
CCFinderは,
ソースコードをトークン単位で直接比較することにより
クローンを検出
名前空間の正規化
ユーザ定義名の置き換え
テーブル初期化部分を取り除く
モジュールの区切りを認識する
数百万行規模のシステムでも実用時間で解析可能
2020/4/26
電子情報通信学会 研究報告
9
CCFinderの処理概要
Source files
static
throws
{{{ String
1. static
static
foo()
throws
RESyntaxException
{
{{ $$ $$
(( (( )) ))throws
$void
void
foofoo()
throws$$RESyntaxException
RESyntaxException
String aaa {
static
RESyntaxException
String
1.
void
throws
RESyntaxException
static void
$ $$foo
throws
]{{] {${$ "123,400"
,
[[ [][] String
$String
$$ =
new
$[] {
]]] === a[]
new
$[]
2. [[String
a[]
=
new
String
{ "123,400",
"123,400",
"abc", "orange
"orange 100"
100" };
};
2.
new
"abc",
[String
$String
"abc"
,
"orange
100"
}
;
org
.
apache
.
regexp
;
}
} ;
3. org.apache.regexp.RE
org.apache.regexp.RE
pat == new
new org.apache.regexp.RE("[0-9,]+");
org.apache.regexp.RE("[0-9,]+");
3.
pat
$$ === new
$$ pat
RE
pat
RE
new
4. .int
int
sum
0; org . apache . regexp
4.
sum
==new
0;
. RE
)) ;;; int
$$ ==== $0$00
$$ sum
$$
$$ ((( "[0-9,]+"
RE
int
sum
sum
5. for
for
(int"[0-9,]+"
i == 0;
0; ii) << a.length;
a.length;
++i)
5.
(int
i
++i)
;; for
$$ i$$i === 0$0$ ;;; i$$i <<<<
int
for (( int
6. a$ ifif. (pat.match(a[i]))
(pat.match(a[i]))
6.
;++
$$ ii)) ))if
$$ ;; ++
length
; ++
++
pat
pat
$ . length
if ifif(( ((($$ pat
7. .. match
sum
+=
Sample.parseNumber(pat.getParen(0));
7.
sum
+=
Sample.parseNumber(pat.getParen(0));
$$ (( (( $$ aa [[ [[ $$ ii ]] ]] )) )) )) ))) $$sum
sum
sum
. match
8.
System.out.println("sum
" ++ sum);
sum);
8. +=
System.out.println("sum
"getParen
+=
Sample
((( 000
(( $$ .. $$ (( ((pat
$$ .. $.$. parseNumber
parseNumber
pat$$ .=
. getParen
pat
.=
getParen
+=
9. }} )) )) ;; System
(( $$ ((( "sum
.. .$$. println
$$ .. .$$. out
System
out
println
"sum==="""
9.
println
"sum
static
String
$$ $$goo
}}} static
))) ;;; goo(String
$$ void
sum
static void
void
goo
String
10. static
static
void
goo(String
[]((a)
a)((( $$throws
throws
RESyntaxException {{
+++ sum
goo
String
10.
[]
RESyntaxException
static
{{ $$ $$ =
$$RE("[0-9,]+");
throws
RESyntaxException
RE exp
exp ===
[[[ exp
]]] )))=
RESyntaxException
RE
exp
11. a$a$RE
RE
exp
=throws
newRESyntaxException
throws
= {{{ RE
11.
new
RE("[0-9,]+");
new
RE
=
$$ )) ;;)) $$;; $$int
$$ (( "[0-9,]+"
int sum
sum
new
=sum$$=== 000
12. new
int
sum"[0-9,]+"
0;
12.
int
sum
== 0;
$$ i$$i === 0$0$ ;;; i$$i <<<<
for
int
for
((( int
for (int
13. ;;;for
for
0; ii << a.length;
a.length; ++i)
++i)
13.
(int
ii == 0;
$
(
$$ ii)) ))if
$$ ;; ++
if
(
exp
length
; ++
++
if
(
exp
a$a$ ... length
;++
(
exp
if ( $
14. . match
(exp.match(a[i]))
14.
ifif (exp.match(a[i]))
]
$
[
$
(
$
(
a
[
i
]
(
a
[
i
]
sum
sum
. $ ( $ [ $ ] )) )) )) ))) $$sum
15. +=
sum
+=
parseNumber(exp.getParen(0));
15.
sum
+=
parseNumber(exp.getParen(0));
(( .. $$ getParen
$$ (( ( $$exp.. ((. $$exp
+=
getParen
()) 0)) )(( )00 )) ))
parseNumber
exp
getParen
$$$ ... parseNumber
+= parseNumber
16. ;;;System.out.println("sum
System.out.println("sum
==="""""+++++sum
sum);
16.
sum);
$$ =
(( $$ ((++ "sum
.. .$$. println
$$ .. .$$. out
=
System
out
println
"sum
sum
System
"sum
sum
17. }}))) ;;; }}}
17.
字句解析
字句解析
トークン列
トークン列
変換処理
変換処理
変換後トークン列
変換後トークン列
検出処理
検出処理
クローン情報
クローン情報
出力整形処理
出力整形処理
クローンペア位置情報
クローン検出
手順
CCFinderはトークンの列としてコードクローン
を検出するため,検出されたコードクローン
は必ずしも集約には適していない
CCFinderの検出したクローン中に存在する最大
の構造的なまとまりを抽出する
大規模なソフトウェアからでも実用的な時間で構造的
なまとまりを持ったクローンを検出可能
2020/4/26
電子情報通信学会 研究報告
11
コード片1
609:
610:
611:
612:
613:
614:
615:
616:
617:
618:
619:
620:
621:
622:
623:
624:
625:
626:
627:
628:
reset();
grammar = g;
// Lookup make-switch threshold in the grammar generic options
if (grammar.hasOption("codeGenMakeSwitchThreshold")) {
try {
makeSwitchThreshold = grammar.getIntegerOption("codeGenMakeSwitchThreshold");
//System.out.println("setting codeGenMakeSwitchThreshold to " + makeSwitchThreshold);
} catch (NumberFormatException e) {
tool.error(
"option 'codeGenMakeSwitchThreshold' must be an integer",
grammar.getClassName(),
grammar.getOption("codeGenMakeSwitchThreshold").getLine()
);
}
}
CCFinderが検出する
クローンペア
// Lookup bitset-test threshold in the grammar generic options
if (grammar.hasOption("codeGenBitsetTestThreshold")) {
try {
bitsetTestThreshold = grammar.getIntegerOption("codeGenBitsetTestThreshold");
コード片2
623:
624:
625:
626:
627:
628:
629:
630:
631:
632:
633:
634:
635:
636:
637:
638:
639:
640:
641:
642:
}
// Lookup bitset-test threshold in the grammar generic options
if (grammar.hasOption("codeGenBitsetTestThreshold")) {
try {
bitsetTestThreshold = grammar.getIntegerOption("codeGenBitsetTestThreshold");
//System.out.println("setting codeGenBitsetTestThreshold to " + bitsetTestThreshold);
} catch (NumberFormatException e) {
tool.error(
"option 'codeGenBitsetTestThreshold' must be an integer",
grammar.getClassName(),
grammar.getOption("codeGenBitsetTestThreshold").getLine()
);
}
}
// Lookup debug code-gen in the grammar generic options
if (grammar.hasOption("codeGenDebug")) {
Token t = grammar.getOption("codeGenDebug");
if (t.getText().equals("true")) {
提案手法が検出す
るクローン
コード片3
1007:
1008:
1009:
1010:
1011:
1012:
1013:
1014:
1015:
1016:
1017:
1018:
1019:
if ( inputState.guessing==0 ) {
buf.append(a.getText());
}
{
_loop144:
do {
if ((LA(1)==WILDCARD)) {
match(WILDCARD);
a=id();
if ( inputState.guessing==0 ) {
buf.append('.'); buf.append(a.getText());
}
}
CCFinderが検出する
クローンペア
コード片4
1527:
1528:
1529:
1530:
1531:
1532:
1533:
1534:
1535:
1536:
1537:
1538:
1539:
if ( inputState.guessing==0 ) {
t=a.getText();
}
{
_loop84:
do {
if ((LA(1)==COMMA)) {
match(COMMA);
id();
if ( inputState.guessing==0 ) {
t+=","+b.getText();
}
}
集約方法の提示
概要
検出したクローンの集約にはリファクタリング
パターンを用いる.
メソッドの抽出: クローンをメソッドとして抽出
メソッドの引き上げ: クローンを親クラスへ引き上
げる
検出したクローンが上記のどちらに適している
か,またはどちらにも適していないのかをメト
リクスを用いて判定する.
2020/4/26
電子情報通信学会 研究報告
14
集約方法の提示
メソッドの抽出
ある部分を新たなメソッドとして抽出するためには,周囲
リファクタリング前
リファクタリング後
との結合度が低いことが望ましい(抽出部分の外側で定
void
methodA(int i){
void methodA(int i){
methodZ();
methodZ();
義された変数を用いていないことが望ましい)
抽出したメソッドの
methodC(i);
呼び出し
System.out.println(“name:” + name);
}
System.out.println(“amount:” + i);
void methodB(int i){
}
methodY();
methodC(i);
メソッドの抽出
void methodB(int i){
}
methodY();
void methodC(int i){
System.out.println(“name:” + name);
System.out.println(“name:” + name);
System.out.println(“amount:” + i);
System.out.println(“amount:” + i);
}
}
2020/4/26
電子情報通信学会 研究報告
15
集約方法の提示
メソッドの引き上げ
リファクタリング前
リファクタリング後
コードクローンを含むクラスが同じクラスを継
承していなければいけない
Employee
Employee
Salesman
メソッドの引き上げ
getName()
Engineer
Salesman
getName()
2020/4/26
Engineer
getName()
電子情報通信学会 研究報告
16
集約方法の提示
コードクローンメトリクス(1/2)
以下の2つについて計測する
結合度の計測: クローンの外側で定義された変数の,
クローンの内側での使用(代入,参照)数
クローンの分散度の計測: クローンが含まれるクラス
がクラス階層においてどのような関係にあるか
解析結果はメトリクスとして数値化する
2020/4/26
電子情報通信学会 研究報告
17
クローンセットS
集約方法の提示
1にはコード片f1とf2が含まれている.
コード片f1内では外部定義の変数a,bをそれぞれ1,
メトリクスRVK(S),RVN(S)
2回使用している.
コード片f2内では外部定義の変数c,dをそれぞれ3,
, fnを含んでいる.
,
Sはコード片f 1, f 2・・・
クローンセット
4回使用している.
i
i
i
miを使用している.
v
,
,
iでは外部定義の変数v 1, v 2・・・
f
コード片
RVK(S ) : (2 + 2)/2 = 2
1
UCfi (v)はコード片fiにおける変数vの使用回数を表す.
また,
RVN(S ) : ((1 + 2) + (3 + 4))/2 = 5
1
このとき,
1 n
RVK(S) mi
n i 1
1 n mj
RVN(S) UCfi(v i j)
n i 1 j 1
.
となる
2020/4/26
電子情報通信学会 研究報告
18
クローンセットS
集約方法の提示
2内の全てのコード片が,ある1つの
クラス内に存在する場合
メトリクスDCH(S)
DCH(S ) : 0
2
クローンセットS3内の全てのコード片があるクラスと
クローンセット
Sはコード片f 1, f 2・・・
,
, fnを含んでいる.
その子クラス内に存在する場合
コード片
fiはクラスCiに存在する.
DCH(S ) : 1
3
C1, C 2・・・
,
, Cnの共通の親クラスのうち,
クローンセットS4内のコード片が含まれるクラスが共
クラス階層的に最も下位に位置するクラスをCpとする.
通の親を持たない場合
クラスCkとクラスChのクラス階層における距離をD (Ck , Ch)とする.
DCH(S4) : -1
このとき,
DCH ( S ) max( D (C1, Cp ), D (C 2, Cp ),・・・, D(Cn, Cp ))
C1, C 2・・・
,
, Cnが共通の親クラスを持たないとき,
DCH ( S ) 1
とする
2020/4/26
電子情報通信学会 研究報告
19
リファクタリング支援ツール
Cancer(概要)
対象言語:
Java
宣言単位
クローン検出部には既存のコードクローン検出ツール
class宣言,interface宣言
CCFinderを利用
ツールの記述言語:
メソッド単位Java
構文,意味解析を行うコンポーネントはオープンソースの
メソッド本体,コンストラクタ,スタ
構文解析器生成ツールJavaCCを用いて構築
ティックイニシャライザ
動作環境:JDK1.4以上のVMが実行可能な環境
サイズ
文単位
解析部: 約15000行
do文,for文,if文,switch文,
GUI部:
約8000行
ユーザはGUIを操作することで,コードクローンの絞り込
synchronized文,try文,while文
みを行うことができる
2020/4/26
電子情報通信学会 研究報告
20
リファクタリング支援ツール
Cancer(メトリクス)
LEN(S): クローンセット S 内の1要素のトークン数
POP(S): クローンセット S 内の要素数
DFL(S): クローンセット S 内の全要素を新しい一
つのサブルーチンにマージした場合のトークン減
少数の予測値
new sub routine
caller statements
2020/4/26
電子情報通信学会 研究報告
21
メトリクスグラフ
クローンセットリスト
クローンユニット選択チェックボックス
クローンセットの絞り込み(1/2)
S1
S2
LEN
2020/4/26
POP
DFL
RVK
RVN
DCH
電子情報通信学会 研究報告
23
クローンセットの絞り込み(2/2)
While 文
S1
S2
メソッド本体
LEN
POP
Method
DFL
While
RVK
RVN
DCH
・・・・・・・・・・・・・・
・・・・・・・・・・・・・
2020/4/26
電子情報通信学会 研究報告
24
コードフラグメントリスト
ソースコードビュー
使用変数リスト
適用実験
概要
Ant (version 1.6.0)
入力
ソースファイル数: 627個
総行数: 約18万行
構造的なクローン検出には約30秒
151個のクローンセットを検出
実験環境
2020/4/26
FreeBSD 4.9
CPU Xeon2.8G×2
メモリ 4GB
電子情報通信学会 研究報告
26
適用実験
Ant(メソッドの抽出)
「メソッドの抽出」を適用するためにクローンを絞
り込んだ条件は次の3つ
文単位のクローンである
全てのコード片が1つのクラス内に存在する
クローンユニット選択チェックボックスで文単位のクローンに
チェックを入れる
メトリクスグラフのDCHの上限を1より小さくする.
クローンの外部で定義されたローカル変数を高々1つ
しか使用していない
メトリクスグラフのRVKの上限を2より小さくする.
151個のうちの32個が該当した
27
適用実験
Ant(メソッドの抽出)
if (!isChecked())
ifif (iSaveMenuItem
(name
// javacopts
== null)
{
{ == null) {
32個の内訳は以下の通り
// make
try
if (javacopts
(other.name
{ sure we don't
!=!=
null
null)
have
&&{ !javacopts.equals(""))
a circular reference here
{
Stack stkiSaveMenuItem
genicTask.createArg().setValue("-javacopts");
return
= newfalse;
Stack(); = new MenuItem();
変数への代入
ローカル変数 stk.push(this);
}
iSaveMenuItem.setLabel("Save
genicTask.createArg().setLine(javacopts);
BuildInfo To Repository");
}dieOnCircularReference(stk,
else}}ifcatch
(!name.equals(other.name))
(Throwable iExc) getProject());
{
{
}
return
handleException(iExc);
false;
}
}
分類
数
}
抽出のみ
外部定義の変数を引数として抽出
引数とした変数を返り値として返す
その他
2020/4/26
電子情報通信学会 研究報告
3個
18個
7個
4個
28
まとめと今後の課題
まとめ
大規模ソフトウェアに対しても実用時間で適用可能な
リファクタリング支援手法を提案しツールを試作した
実際のソフトウェアに対して適用実験を行い,絞り込
んだうちの多くのクローンについて集約を行った
今後の課題
ソフトウェアの品質・複雑度などの視点からそのク
ローンの集約を行うべきかを評価
2020/4/26
電子情報通信学会 研究報告
29
2020/4/26
電子情報通信学会 研究報告
30
リファクタリング支援ツール
Cancer(解析の流れ)
解析部
GUI部
コードクローン検出
クローン情報 構造的なクローン抽出
メトリクス値の計算
メトリクス情報の
ついた構造的な
クローン情報
ソースコード
構造的なまとまりの抽出
クラス階層情報の抽出
変数定義情報の抽出
2020/4/26
ユ
ー
ザ
イ
ン
タ
ー
フ
ェ
ー
ス
ユーザ
構造情報
電子情報通信学会 研究報告
31
適用実験
Ant(「メソッドの抽出」)
if (errorProperty != null) {
errorBaos = new ByteArrayOutputStream();
managingTask.log("Error redirected to property: " + errorProperty,
Project.MSG_VERBOSE);
if (error == null) {
errorStream = errorBaos;
} else {
errorStream = new TeeOutputStream(errorStream, errorBaos);
}
} else {
errorBaos = null;
}
2020/4/26
電子情報通信学会 研究報告
32
適用実験
Ant(「メソッドの抽出」)
if (append != null) {
if (!append.isAbsolute()) {
append = new File(getProject().getBaseDir(), append.getPath());
}
appendReader = new BufferedReader(new FileReader(append));
}
2020/4/26
電子情報通信学会 研究報告
33
適用実験
Ant(「メソッドの抽出」)
if (className != null) {
try {
// load the specified Cache, save the reference and configure it
cache = (Cache) Class.forName(className).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
2020/4/26
電子情報通信学会 研究報告
34
適用実験
Ant(メソッドの引き上げ)
「メソッドの引き上げ」を適用するためにクローン
を絞り込んだ条件は次の2つ
メソッド単位のクローンである
クローンの存在するクラスが共通の親クラスを持つ
151個のうちの20個が該当した
2020/4/26
電子情報通信学会 研究報告
35
適用実験
Ant(メソッドの引き上げ)
20個の内訳は以下の通り
分類
引き上げのみ
外部定義の変数を引数として引き上げ
引数にした変数を返り値として返す
その他
2020/4/26
電子情報通信学会 研究報告
数
0個
10個
2個
8個
36
適用実験
Ant(メソッドの引き上げ)
private void getCommentFileCommand(Commandline cmd) {
if (getCommentFile() != null) {
/* Had to make two separate commands here because if a space is
inserted between the flag and the value, it is treated as a
Windows filename with a space and it is enclosed in double
quotes ("). This breaks clearcase.
*/
cmd.createArgument().setValue(FLAG_COMMENTFILE);
cmd.createArgument().setValue(getCommentFile());
}
}
自クラスへのメソッド呼び出し
2020/4/26
自クラスへのフィールド変数
電子情報通信学会 研究報告
37
適用実験
Ant(メソッドの引き上げ)
public void verifySettings() {
if (targetdir == null) {
setError("The targetdir attribute is required.");
}
if (mapperElement == null) {
map = new IdentityMapper();
} else {
自クラスへのフィールドへの代入
map = mapperElement.getImplementation();
}
if (map == null) {
setError("Could not set <mapper> element.");
}
}
2020/4/26
電子情報通信学会 研究報告
38
適用実験
Ant(メソッドの引き上げ)
public void execute() throws BuildException {
Commandline commandLine = new Commandline();
Project aProj = getProject();
int result = 0;
// Default the viewpath to basedir if it is not specified
if (getViewPath() == null) {
setViewPath(aProj.getBaseDir().getPath());
}
// build the command line from what we got. the format is
// cleartool checkin [options...] [viewpath ...]
// as specified in the CLEARTOOL.EXE help
commandLine.setExecutable(getClearToolCommand());
commandLine.createArgument().setValue(COMMAND_CHECKIN);
checkOptions(commandLine);
自クラスのメソッド呼び出し
result = run(commandLine);
if (Execute.isFailure(result)) {
String msg = "Failed executing: " + commandLine.toString();
throw new BuildException(msg, getLocation());
}
}
2020/4/26
電子情報通信学会 研究報告
39
リファクタリング支援ツール
Cancer(計算コスト)
O(nt)
n: ソースコードのトークン数
解析部
t: 検出した最も長いクローンのトークン数
コードクローン検出
クローン情報
構造的なクローン抽出
メトリクス値の計算
O(cs log c)
構造的なまとまりの抽出
ソースコード
クラス階層情報の抽出
変数定義情報の抽出
c: ファイル1つあたりに含まれるコードクローンの数
メトリクス情報の
s: ソースファイル数
ついた構造的な
クローン情報
O(n)
構造情報
n: ソースコードのトークン数
2020/4/26
電子情報通信学会 研究報告
40
Suffix-tree
Suffix tree is a tree that satisfies
the following conditions.
A leaf node represents the starting
position of sub-string.
A path from root node to a leaf node
represents a sub-string.
First characters of labels
of all the edges from one node
are different from each other.
xyxyz% 1
xyz% 2
x
→ A common path means a clone
1 2 3 4 5 6 7
y
x x y x y z %
1 x *
z%
2 x * *
3 y
*
6
4 x * *
*
5 y
*
*
%
6 z
*
7
2020/4/26
電子情報通信学会
研究報告
7 %
*
y
xyz%
z%
3
4
z%
5
1 2 3 4 5 6 7
41
x x y x y z%